Skip to content

feat(#2088): migrate marts__micromasters_dedp_exam_grades to dim_course_run - #2403

Open
quazi-h wants to merge 12 commits into
mainfrom
copilot/2088-migrate-dedp-exam-grades-mart
Open

quazi-h wants to merge 12 commits into
mainfrom
copilot/2088-migrate-dedp-exam-grades-mart

Conversation

@quazi-h

@quazi-h quazi-h commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What are the relevant tickets?

Partially addresses #2088 (follow-up to #2319dim_course_run semester/passing_grade)

Description (What does it do?)

Migrates the MITxOnline branch of marts__micromasters_dedp_exam_grades to read semester and passing_grade from dim_course_run instead of re-deriving them through a duplicate join to MicroMasters staging inside int__mitxonline__proctored_exam_grades.

#2319 added those columns to dim_course_run but populated semester from micromasters_examruns only, dropping the courserun_tag fallback the old mart applied to course runs with an embedded proctored-exam unit — which is why a straight swap nulled 8,137 rows when Kate first tried it. dim_course_run now restores that fallback, gated on the same course-structure predicate the intermediate model used (a block titled 'proctored exam', read from its newest snapshot per block) so it only applies to runs that actually have an exam — 100 course runs, not all 4,513. passing_grade deliberately gets no fallback: the old model emitted null on that branch, so a bare er.examrun_passing_grade reproduces it. A new singular test asserts the dimension keeps covering every run the upstream reports with a semester.

Mart output is unchanged and the dimension changes in exactly one column, both reproduced in step 3. Expect a one-time SCD2 re-version of 87 dim_course_run rows on the first production run after merge.

How can this be tested?

Everything below runs on this PR's branch — the baseline side is read out of origin/main with git show, so there is no branch switching. Start from the repo root; step 1 cds into src/ol_dbt and steps 2 and 3 continue from there.

1. Register the sources and build both sides of the comparison.

# Skip if a worktree already holds this branch — cd into it instead.
gh pr checkout 2403

# The baseline is read from origin/main, so it has to be current.
git fetch origin main

# override_ref.sql resolves unchanged ancestors to production Glue views, so only these
# three layers need registering. Register immediately before building.
for db in staging intermediate dimensional; do
  uv run --frozen ol-dbt local register --database ol_warehouse_production_$db
done

# Pre-migration copy of each changed model, from origin/main. Absolute paths so they
# survive the cd below.
R=$(git rev-parse --show-toplevel)
MART=$R/src/ol_dbt/models/marts/micromasters/marts__micromasters_dedp_exam_grades
DIM=$R/src/ol_dbt/models/dimensional/dim_course_run
git show origin/main:src/ol_dbt/models/marts/micromasters/marts__micromasters_dedp_exam_grades.sql > ${MART}_pre.sql
git show origin/main:src/ol_dbt/models/dimensional/dim_course_run.sql > ${DIM}_pre.sql

# One invocation, so all four read identical sources. --full-refresh is required because
# dim_course_run is incremental and would otherwise merge without re-deriving semester.
cd "$R/src/ol_dbt"
uv run --frozen dbt run -t dev_local --full-refresh --select \
  dim_course_run dim_course_run_pre \
  marts__micromasters_dedp_exam_grades marts__micromasters_dedp_exam_grades_pre

PASS=5.

2. Test the two models that changed.

# buildable, not cautious (drops this PR's new test) and not eager (adds 8 relationships_*
# tests that report orphans by construction; one scans tfact_grade, 42.7M rows).
uv run --frozen dbt test -t dev_local --indirect-selection=buildable --select \
  dim_course_run marts__micromasters_dedp_exam_grades

PASS=13 WARN=1 ERROR=0, including assert_dim_course_run_semester_covers_proctored_exam_runs. The warning is not_null_dim_course_run_course_fk, pre-existing and identical on main.

3. Diff both changed models, old against new.

The mart must not change:

uv run --frozen ol-dbt diff \
  --old marts__micromasters_dedp_exam_grades_pre \
  --new marts__micromasters_dedp_exam_grades \
  -k user_micromasters_email,user_mitxonline_email,proctoredexamgrade_created_on

MATCH, 12 columns, 20867 / 20867 Δ=0, exit 0. Negative control: replace semester with a literal in ${MART}_pre.sql, rebuild that model, rerun — it reports mismatch.

dim_course_run must change, in one column only:

# effective_date/end_date are current_timestamp, so they differ on every build.
uv run --frozen ol-dbt diff --old dim_course_run_pre --new dim_course_run \
  -k courserun_pk --exclude-columns effective_date,end_date

MISMATCH, exit 1 — expected here. 8510 / 8510 Δ=0, one column moving: semester: 1.02% (87 rows), samples all None → '<tag>'. No passing_grade. Non-null semester 12 → 99.

Both diffs are needed: the mart diff proves nothing about dim_course_run, which has eight other downstream consumers.

rm ${MART}_pre.sql ${DIM}_pre.sql

Two known snags: ~/.ol-dbt/local.duckdb is shared across worktrees, so if ol-dbt diff fails with a DuckDB Conflicting lock error another session has it open — rerun when free. And a build failing on an HTTP 404 for a __dbt_tmp metadata path just means that layer's registration went stale; re-register it and rerun.

🤖 Generated with Claude Code

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 the marts__micromasters_dedp_exam_grades mart so the MITxOnline branch sources semester and passing_grade from the dimensional model dim_course_run (intended to centralize and de-duplicate that derivation).

Changes:

  • Add a dim_course_run-backed CTE for MITxOnline semester/passing_grade.
  • Replace MITxOnline output columns to read from dim_course_run instead of the intermediate proctored-exam grades model.
  • Keep the pure-MicroMasters branch unchanged (still sourcing those fields directly from the MicroMasters intermediate model).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ol_dbt/models/marts/micromasters/marts__micromasters_dedp_exam_grades.sql Outdated
Comment thread src/ol_dbt/models/marts/micromasters/marts__micromasters_dedp_exam_grades.sql Outdated

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

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

Comment thread src/ol_dbt/models/marts/micromasters/marts__micromasters_dedp_exam_grades.sql Outdated
Comment thread src/ol_dbt/models/marts/micromasters/marts__micromasters_dedp_exam_grades.sql Outdated

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

Copilot reviewed 1 out of 2 changed files in this pull request and generated no new comments.

@quazi-h
quazi-h marked this pull request as ready for review July 7, 2026 21:38
@KatelynGit KatelynGit self-assigned this Jul 8, 2026
@blarghmatey
blarghmatey force-pushed the copilot/2088-migrate-dedp-exam-grades-mart branch from d64395e to 1ba8823 Compare July 16, 2026 14:06
@github-actions

Copy link
Copy Markdown

🔎 ol-dbt impact — column-level blast radius

✅ No column-level downstream impact detected for the changed models.

Posted by ol-dbt impact (annotate-only — does not block merge).

@quazi-h
quazi-h force-pushed the copilot/2088-migrate-dedp-exam-grades-mart branch from b61973a to f0015bf Compare September 1, 2026 19:07
quazi-h added a commit that referenced this pull request Sep 1, 2026
dim_course_run.semester was NULL for MITxOnline course runs whose
proctored exam is an embedded unit in the course rather than a dedicated
MicroMasters exam-run course object: those runs have no
micromasters_examruns match, so er.examrun_semester is NULL.

#2319 moved this field into dim_course_run but populated it only from
the exam-run join, dropping the fallback branch that
int__mitxonline__proctored_exam_grades.sql had. That regression blocks
#2088 / PR #2403, which reads semester from dim_course_run: 72 of the
84 affected course runs came through with no semester at all.

Restore the original derivation with coalesce(..., cr.courserun_tag).

Validated on dev_local against production-shaped data: a full-refresh
rebuild of dim_course_run and marts__micromasters_dedp_exam_grades
diffs row-for-row clean against the pre-migration baseline snapshot --
20908/20908 rows, all 12 columns identical, zero NULL semesters on
either side. dbt tests for both models pass, including the
dbt_expectations compound-uniqueness check on the mart, so the coalesce
does not fan out rows.

Refs #2088, #2319

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quazi-h added a commit that referenced this pull request Sep 1, 2026
…s-db macro

dim_course_run.sql was the last model still calling regexp_like() raw.
Every other call site already dispatches through the macro in
macros/cross_db_functions.sql, which maps to regexp_matches() on DuckDB;
DuckDB has no regexp_like function, so the raw call makes the model
unbuildable on the dev_local target:

    Catalog Error: Scalar Function with name regexp_like does not exist!

That blocks local validation of #2088 / PR #2403, which reads
dim_course_run. The macro is already on main -- this is a call-site
consistency fix, not a new dependency, and it is independent of the
in-flight to_iso8601 migration in PR #2600.

Refs #2088

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quazi-h
quazi-h requested review from KatelynGit and a balanced review from Copilot September 2, 2026 20:50

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.

🟡 Changes recommended

The semester fallback currently populates every unmatched MITxOnline course run, contrary to the documented exam-only contract.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 2/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/ol_dbt/models/dimensional/dim_course_run.sql Outdated
@quazi-h
quazi-h force-pushed the copilot/2088-migrate-dedp-exam-grades-mart branch from f0015bf to 53a4932 Compare September 9, 2026 18:10
quazi-h added a commit that referenced this pull request Sep 9, 2026
dim_course_run.semester was NULL for MITxOnline course runs whose
proctored exam is an embedded unit in the course rather than a dedicated
MicroMasters exam-run course object: those runs have no
micromasters_examruns match, so er.examrun_semester is NULL.

#2319 moved this field into dim_course_run but populated it only from
the exam-run join, dropping the fallback branch that
int__mitxonline__proctored_exam_grades.sql had. That regression blocks
#2088 / PR #2403, which reads semester from dim_course_run: 72 of the
84 affected course runs came through with no semester at all.

Restore the original derivation with coalesce(..., cr.courserun_tag).

Validated on dev_local against production-shaped data: a full-refresh
rebuild of dim_course_run and marts__micromasters_dedp_exam_grades
diffs row-for-row clean against the pre-migration baseline snapshot --
20908/20908 rows, all 12 columns identical, zero NULL semesters on
either side. dbt tests for both models pass, including the
dbt_expectations compound-uniqueness check on the mart, so the coalesce
does not fan out rows.

Refs #2088, #2319

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quazi-h added a commit that referenced this pull request Sep 9, 2026
…s-db macro

dim_course_run.sql was the last model still calling regexp_like() raw.
Every other call site already dispatches through the macro in
macros/cross_db_functions.sql, which maps to regexp_matches() on DuckDB;
DuckDB has no regexp_like function, so the raw call makes the model
unbuildable on the dev_local target:

    Catalog Error: Scalar Function with name regexp_like does not exist!

That blocks local validation of #2088 / PR #2403, which reads
dim_course_run. The macro is already on main -- this is a call-site
consistency fix, not a new dependency, and it is independent of the
in-flight to_iso8601 migration in PR #2600.

Refs #2088

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quazi-h

quazi-h commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer input wanted: dim_course_run.semester semantics (f3909a6)

This PR changes what dim_course_run.semester means for MITxOnline, and I'd like a second opinion on the call rather than landing it quietly.

What changed. dim_course_run.sql now uses coalesce(er.examrun_semester, cr.courserun_tag). Measured on dev_local, that moves semester from 12 populated to 4,513 of 4,513 mitxonline is_current rows — 4,501 of them via the courserun_tag fallback.

Why it's needed. It isn't new behaviour, it's restored behaviour. int__mitxonline__proctored_exam_grades is a UNION whose second branch has derived courserun_tag as semester (line 66) since well before #2319. #2319 populated the dimension from MicroMasters exam runs only and dropped that fallback — which is why semester was ~99.7% NULL, and why migrating the mart onto it without this fix would have nulled out live values that users currently see.

What I corrected. _dim_course_run.yml documented the column as "Null for all other platforms and non-exam MITxOnline course runs". The second half is now false for 4,501 rows, so the description is rewritten to describe the actual derivation. Worth knowing that description was added in ac99b174 — the same commit as the column — so it described that implementation rather than an agreed contract, and nothing enforces it (ol-dbt validate checks column presence and sync, never semantics).

The judgement call. Should semester mean "this course run's academic term" (broad, what it now does), or stay narrowed to "the MicroMasters proctored exam run's semester" (the old documented wording, restoring NULLs for non-exam runs)?

I went with broad, for three reasons: it reproduces the derivation users have actually been served all along; courserun_tag genuinely is the run's term designation; and narrowing it would need a subquery against exam-unit grades to reconstruct the old branch condition, adding complexity that benefits no current consumer.

Blast radius is small, and I verified it rather than assuming. marts__micromasters_dedp_exam_grades is the only model in the repo that selects dim_course_run.semester — 14 other models reference dim_course_run but never that column — and it reads just 84 distinct course runs. A whole-row EXCEPT ALL between the pre-migration and migrated mart returns 0 rows in both directions, so today's data is provably unchanged. The exposure is prospective: a future consumer of dim_course_run.semester will now see values where it previously saw NULL.

If you'd rather keep the column narrow, say so and I'll implement the restricted fallback instead — the mart's output is identical either way, so this is purely about what the dimension promises.

quazi-h added a commit that referenced this pull request Sep 11, 2026
Copilot review on #2659 found five overclaims. All five are valid.

- The incremental trap was framed as "an unchanged key set makes the run a
  no-op". dbt does execute the model SQL; what varies is which rows the
  model's own is_incremental() predicate reselects. Reframed around the
  predicate, with the two real failure modes (excluded rows keep old-code
  values; a predicate that matches everything yields a true no-op) and the
  SCD2 consequence that dim_course_run appends a second generation rather
  than replacing the first. Stopped asserting a root cause for #2403's 0.11s
  no-op that was never established — the lesson is not to reason from the
  predicate at all.
- "Both sides read the identical polluted source and the duplication
  cancels" is false exactly where a #2072 migration re-points ref(), which
  is every migration PR. override_ref resolves each unbuilt ref() to its own
  glue__ view with independent __dbt_tmp pollution, and joins/filters mean
  the two do not offset. Added: build the divergent upstreams locally, or
  label the result unverified source noise rather than accepting/rejecting
  on the registration artifact the section warns about.
- Step 3 used `dbt build`, contradicting the cautious-test section below it
  and risking an unmaterialized comparison side when a test fails. Now
  `dbt run`.
- "Row-count delta 0 means the grain is intact" — dropped rows offset by
  duplicates net to zero too. Now count parity, with a
  count(*) = count(distinct key) check on both relations.
- Step 6's distinct-pair assertion cancels duplication but not rows a
  polluted view is missing; scoped accordingly.

Also propagated the incremental correction to ol-dbt-local-dev, including
its two worked examples, which both rebuilt without --full-refresh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e tag fallback

The description claimed "Null for all other platforms and non-exam MITxOnline
course runs". The coalesce this PR adds makes the second half false: measured on
dev_local, semester is populated for 4,513 of 4,513 mitxonline is_current rows,
4,501 of them via the courserun_tag fallback rather than a MicroMasters exam-run
match.

The description was not a contract this PR violates. It was added in ac99b17
(#2319), the same commit that added the column, and it described that commit's
implementation — which populated semester only from micromasters_examruns and
silently dropped the courserun_tag fallback that
int__mitxonline__proctored_exam_grades line 66 had been applying all along. So
the documentation recorded an implementation gap as if it were intended
behaviour. This PR restores the original derivation; the description now matches
what the column actually contains.

"Null for all other platforms" is unchanged and still true — edxorg, mitxpro,
residential and bootcamps each set semester to null explicitly, and all four
measure 0% populated.

Note passing_grade's near-identical wording is deliberately left alone: it takes
no coalesce, because the pre-migration model's second branch emitted
`null as proctoredexamgrade_passing_grade`. Its description remains accurate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quazi-h
quazi-h force-pushed the copilot/2088-migrate-dedp-exam-grades-mart branch from f3909a6 to 393f026 Compare September 14, 2026 13:28
…ve a proctored exam

The courserun_tag fallback added earlier on this branch was applied to every
MITxOnline course run, because int__mitxonline__course_runs is every MITxOnline
course run and there was no exam predicate. That populated semester for 4,513 of
4,513 current MITxOnline runs where only 12 had a value before — including ~2,730
UAI partner cohorts and other runs with no proctored exam at all, for which a term
label is meaningless. Over half those new values were not even clean term
identifiers (3T2026_AGCO_C1, 2013_Spring, T12026).

int__mitxonline__proctored_exam_grades already had the right predicate: its
exam_unit_grades CTE identifies embedded proctored exams by a course structure
block titled 'proctored exam'. Reusing it against int__mitxonline__course_structure
(a pure structure model — no grades dependency, no cycle) narrows the fallback to
exactly the runs it was meant for.

Measured on dev_local:

    semester populated   4,513 -> 100   (97 clean terms, 3 suffixed '1T2022a')
    left null            0     -> 4,413
    other platforms      0 populated (unchanged)

marts__micromasters_dedp_exam_grades is unaffected: all 84 course runs it consumes
fall inside the 100. Whole-row symmetric EXCEPT ALL against the pre-migration mart
built from one registration in one invocation returns 0 rows in both directions
across all 12 columns, and semester is still 0% null on both sides. So this keeps
the property that made the migration safe while dropping the over-broad write.

Also shrinks the one-time SCD2 re-version on deploy from 4,501 rows to ~100, since
semester participates in the change-detection predicate.

Adds assert_dim_course_run_semester_covers_proctored_exam_runs. The gate is a live
lookup against course structure rather than a static property, so if structure data
stops reporting the block for a run that genuinely has proctored exam grades,
semester would silently go null and the mart would quietly lose values — the same
shape as the 8,137-null regression reported on this PR in July. Verified the test
actually fires: stubbing the gate predicate fails it with 72 results, the exact
count of affected runs; restored, it passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quazi-h added a commit that referenced this pull request Sep 15, 2026
…n PRs

The two existing dbt skills drive the tools (`ol-dbt-local-dev` for
register/build, `ol-dbt-fast-validation` for validate/impact/diff) but neither
answers the question a migration PR actually turns on: is the data the same?
This adds the acceptance procedure, derived from validating #2403.

The load-bearing part is which local numbers are trustworthy. A fresh register
of staging/intermediate/dimensional on 2026-09-09 still put 624 of 640 non-raw
Glue views on `__dbt_tmp` metadata locations, which return duplicated and
partially-missing rows while the build succeeds. So an absolute row count or
fill rate read through a `glue__` view is not evidence, and several have already
been quoted in PR bodies as though they were. A difference between two locally
built sides of the SAME registration is evidence, because the pollution cancels.
The skill is organised around that distinction: register once, build both sides
in one invocation, compare.

Also records three traps that each cost real time:

- `--full-refresh` is mandatory when the model under test is incremental.
  #2403's own documented test command omits it, and `dim_course_run` therefore
  merged in 0.11s without re-deriving the column under test. This contradicts
  `ol-dbt-local-dev`'s "prefer incremental" rule, which is right for iterating
  and wrong for validating; the new skill says so explicitly rather than
  silently disagreeing.
- The per-column multiset diff needs no join key at all and localises a
  difference to one column in a single pass.
- A column unpopulated on both sides is unverified, not passing.

Registers the skill in agent-config.toml and adds a `migration` profile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quazi-h added a commit that referenced this pull request Sep 15, 2026
… test-selection traps

Follow-up to the initial draft, from validating PR #2403 end to end.

Amends `ol-dbt-local-dev`'s rule rather than leaving the new skill to contradict
it. The old wording — "reserve --full-refresh for when the incremental state is
stale or wrong" — cannot fire, because the failure mode it needs to catch is a
state that is perfectly valid and merely does not reflect your new code. An
incremental run whose key set is unchanged is a no-op that still reports OK, so
a changed expression is never re-evaluated and you read the old values believing
they are new. #2403's own documented test command hit exactly this:
dim_course_run merged in 0.11s and never re-derived the column under test, for
two months. The rule is now framed as iterating vs concluding, which is the
distinction that actually decides it.

Adds two traps neither existing skill covers:

- ~/.ol-dbt/local.duckdb is shared by every worktree and session on the machine.
  A concurrent session rebuilt dim_course_run from main mid-validation and
  semester went from 4,513/4,513 populated to 12/4,513 with no warning from
  either side. This is the second independent reason to materialize both
  comparison sides in ONE invocation: the #2403 result survived only because both
  marts were already physical tables.
- dbt defaults to --indirect-selection=eager, which pulls in relationships_*
  tests owned by other models and compares your local build against production
  Glue views. Measured 20 tests eager vs 11 cautious on #2403's two models; the 9
  difference were all cross-model, including a 42.7M-row tfact_grade scan and
  every failure the PR body documented as expected. Also records why
  expected-failure counts do not belong in a PR body — #2403 documented 87/74/13
  and the same tests produced 861/178/150 plus an undocumented fourth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quazi-h added a commit that referenced this pull request Sep 15, 2026
Copilot review on #2659 found five overclaims. All five are valid.

- The incremental trap was framed as "an unchanged key set makes the run a
  no-op". dbt does execute the model SQL; what varies is which rows the
  model's own is_incremental() predicate reselects. Reframed around the
  predicate, with the two real failure modes (excluded rows keep old-code
  values; a predicate that matches everything yields a true no-op) and the
  SCD2 consequence that dim_course_run appends a second generation rather
  than replacing the first. Stopped asserting a root cause for #2403's 0.11s
  no-op that was never established — the lesson is not to reason from the
  predicate at all.
- "Both sides read the identical polluted source and the duplication
  cancels" is false exactly where a #2072 migration re-points ref(), which
  is every migration PR. override_ref resolves each unbuilt ref() to its own
  glue__ view with independent __dbt_tmp pollution, and joins/filters mean
  the two do not offset. Added: build the divergent upstreams locally, or
  label the result unverified source noise rather than accepting/rejecting
  on the registration artifact the section warns about.
- Step 3 used `dbt build`, contradicting the cautious-test section below it
  and risking an unmaterialized comparison side when a test fails. Now
  `dbt run`.
- "Row-count delta 0 means the grain is intact" — dropped rows offset by
  duplicates net to zero too. Now count parity, with a
  count(*) = count(distinct key) check on both relations.
- Step 6's distinct-pair assertion cancels duplication but not rows a
  polluted view is missing; scoped accordingly.

Also propagated the incremental correction to ol-dbt-local-dev, including
its two worked examples, which both rebuilt without --full-refresh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quazi-h added a commit that referenced this pull request Sep 15, 2026
…is tuned an order of magnitude too loose

ol-dbt-local-dev told you to register, and `list-sources` warns when the registry
is ">1 day old". Both understate the problem. `register` stores the Iceberg
metadata_location Glue reports at that instant, and for a dbt-built table Glue
routinely points the canonical name at a __dbt_tmp directory that the next
production materialization swaps and deletes.

Measured while validating #2403: a registry refreshed 60 MINUTES earlier failed
with HTTP 404 reading the metadata JSON for int__micromasters__dedp_proctored_exam_grades,
killing two mart models mid-validation. Re-registering only the intermediate layer
(41 pointers moved, 118 unchanged) fixed it. Re-registering the same three layers
26 hours later moved another 336 pointers — 184 staging, 108 intermediate, 44
dimensional — which is the churn rate the one-day threshold is failing to track.

The 404 is the lucky shape. When the __dbt_tmp directory still exists but holds a
mid-build snapshot, the view returns duplicated or partial rows and nothing fails
at all: a clean run and wrong numbers, which is the dangerous case when those
numbers are about to be quoted in a PR. So the guidance is unconditional
re-registration immediately before a build, not a reaction to a visible error.

#2660 stopped __dbt_tmp tables being registered as sources in their own right but
deliberately scoped out canonical names pointing at __dbt_tmp locations (621/636
dbt-built tables at last count); that remains tracked separately. Until it lands,
freshness is the caller's responsibility, so the skill now says so.

Verified the three documented register commands run verbatim from a clean worktree
(0 errors each) and pre-commit passes on the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quazi-h added a commit that referenced this pull request Sep 15, 2026
…ical table is immune

Fourth Copilot round. Both findings valid, both verified before fixing.

The ref-discovery command I added last round only matched single-quoted
ref(). Measured: 13 double-quoted occurrences across 5 models, THREE OF
THEM IN reporting/ -- the layer #2072 migrates. On
reporting/data_detail_problems.sql the shipped pattern returns zero
matches, so the diff shows no divergent refs, the reader omits the
upstreams, and the comparison is uncontrolled. It fails silently and in
the direction that looks clean, which is the worst shape for this
particular command. Now a char-class pattern (portable -- a backreference
errors under ugrep) that also strips quotes and the ref() wrapper, so it
compares bare model names and re-quoting a ref between versions cannot
read as a false divergence. Verified verbatim: finds the double-quoted
refs, still surfaces the real divergence on the ac99b17 -> main
dim_course_run case, clean across 60 models.

"Table-materialized output is immune once written" contradicted this
section's own opening sentence, which says another session's dbt run will
overwrite your tables. It will. A physical table is immune to POINTER ROT,
not to another dbt run. The #2403 marts survived because the concurrent
session rebuilt dim_course_run, their upstream, and never selected the
marts -- targeting, not immunity. Replaced with that distinction plus the
operational requirement: confirm exclusive use of dev_local and measure
immediately, since one invocation narrows the window but is not atomic.
Added real isolation as the option when a result will be published --
HOME=/tmp/iso-validation redirects dev_local to a private DuckDB file
(verified; the 19GB warehouse is untouched), at the cost of a full
re-register since that database starts empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quazi-h
quazi-h requested a balanced review from Copilot September 15, 2026 19:08

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.

🟡 Changes recommended

The regression test can miss absent dimension rows and allows up to ten affected runs without failing.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 4/5 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/ol_dbt/tests/assert_dim_course_run_semester_covers_proctored_exam_runs.sql Outdated
Addresses two Copilot findings on the new singular test.

The project sets `+error_if: ">10"` for all tests (dbt_project.yml), and this
query returns one row per affected course run — so a regression touching up to
ten runs would have downgraded to a warning, which is exactly the silent failure
the test exists to catch. Override error_if/warn_if to `!= 0` locally.

Switch the inner join to a left join: a proctored-exam run with no current
dim_course_run row at all does the same downstream damage as one with a NULL
semester, because the mart left-joins the dimension. The inner join dropped
precisely those rows and passed.

Verified on production Trino: 79 proctored-exam runs, all with a semester, 0
with no current dimension row — so the left join closes the hole without
changing today's result. Test passes against a locally built dim_course_run.

Co-Authored-By: Claude Opus 5 (1M context) <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.

🔵 Needs a closer look

The fallback gate currently includes historical course-structure snapshots instead of only the latest structure.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/ol_dbt/models/dimensional/dim_course_run.sql:56

  • int__mitxonline__course_structure retains every retrieval and marks only the newest snapshot with coursestructure_is_latest (see int__mitxonline__course_structure.sql:7-12,55-60). Without filtering here, a block that existed only in an old snapshot keeps this run in the fallback forever, so semester does not become null when the current structure stops reporting the proctored-exam unit as the new comments and regression test specify. Restrict the gate to the latest structure snapshot.
  • Files reviewed: 4/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

int__mitxonline__course_structure retains every retrieval, so the unfiltered
gate matched a 'proctored exam' block title from any historical snapshot. A run
whose exam block was renamed or removed stayed in the fallback forever, which
contradicts the regression test's stated contract.

Mirror what the upstream actually does. int__mitxonline__proctored_exam_grades
reaches course structure through int__mitxonline__courserun_subsection_grades,
which dedups to the newest snapshot per (course run, block) and matches the
title on that. Reproduce the same dedup rather than filtering on
coursestructure_is_latest: is_latest keeps only blocks present in the newest
whole-course retrieval, which drops runs whose exam block has since left the
structure while their graded attempts remain.

Measured on production 2026-09-15 against the 79 course runs that have
proctored exam grades with a semester:

  gate as shipped (any snapshot)      107 runs, misses 0 of the 79
  coursestructure_is_latest           105 runs, misses 1 of the 79
  newest snapshot per block (this)    106 runs, misses 0 of the 79

The run is_latest would have dropped is course-v1:MITxT+14.310GZ+3T2024, which
has exam grades and a semester the pre-migration mart populated. The run this
change removes is course-v1:MITxT+14.73GZ+1T2025, which has no proctored exam
grades at all, so no mart output changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quazi-h

quazi-h commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Re: Copilot's "fallback gate includes historical course-structure snapshots" — the diagnosis is correct and is fixed in a01101b. The recommended remedy was not, and I measured before applying it.

The diagnosis holds

int__mitxonline__course_structure retains every retrieval. The gate had no snapshot filter, so a run whose exam block was renamed or removed stayed in the fallback forever — which contradicts what the regression test claims to guard.

It also meant my own code comment was false: it said we reuse int__mitxonline__proctored_exam_grades's predicate, and we didn't.

The recommended fix would have caused a regression

coursestructure_is_latest keeps only blocks present in the newest whole-course retrieval. But the upstream doesn't use that flag. int__mitxonline__proctored_exam_grades reaches course structure via int__mitxonline__courserun_subsection_grades, which dedups to the newest snapshot per (course run, block):

row_number() over (partition by courserun_readable_id, coursestructure_block_id
                   order by coursestructure_retrieved_at desc) as row_num
...
where course_structure.row_num = 1

Those differ precisely for a block that has left the structure: per-block dedup still returns its most recent snapshot, is_latest excludes it. Measured on production against the 79 course runs that have proctored exam grades with a semester:

gate runs matched of the 79, missed
any snapshot (as shipped) 107 0
coursestructure_is_latest 105 1
newest snapshot per block (applied) 106 0

The run is_latest drops is course-v1:MITxT+14.310GZ+3T2024 — it has proctored exam grades and a semester the pre-migration mart populated. Filtering on is_latest would have nulled that value, broken migration parity, and failed the regression test added in this PR.

What landed

Mirrored the per-block dedup. The only run removed is course-v1:MITxT+14.73GZ+1T2025, which has no proctored exam grades, so no mart output changes — the migration parity result stands.

Verified against a locally built dim_course_run:

run semester expected
course-v1:MITxT+14.310GZ+3T2024 3T2024 retained ✓
course-v1:MITxT+14.73GZ+1T2025 NULL removed ✓

assert_dim_course_run_semester_covers_proctored_exam_runs still passes. The code comment now explains why coursestructure_is_latest is deliberately not used, so the next reader doesn't re-apply it.

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.

🟡 Changes recommended

The regression test derives its expected rows through the same course-structure predicate, so it cannot detect the documented source-drift failure.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 4/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

…rift

The test's header said it fires when course structure stops reporting the
'proctored exam' block. It does not, and cannot: dim_course_run's gate and
int__mitxonline__proctored_exam_grades' exam_unit_grades CTE read the same
predicate, so drift removes the run from both sides at once. The upstream stops
reporting it, the query has nothing left to select, and the test passes.

Measured on production 2026-09-15: 72 of the 79 course runs carrying a semester
depend on that shared predicate. Only the 7 with a MicroMasters exams_examrun
record are independent of course structure.

The earlier negative control did not expose this because it stubbed the gate in
dim_course_run only, breaking the symmetry the real failure preserves.

No query change — what the test does catch is real and worth keeping: divergence
between the dimension's hand-reproduced gate and the upstream derivation, plus
any run missing a current dimension row. The header now states that scope,
records why drift is invisible here, and notes that drift removes mart rows
outright and has to be caught upstream against a signal independent of the
current block title.

Co-Authored-By: Claude Opus 5 (1M context) <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.

🟢 Approval recommended

The current revision resolves the previously identified correctness gaps and preserves the mart’s established output semantics.

Review details
  • Files reviewed: 4/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@quazi-h

quazi-h commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Out of scope for this PR, but directly relevant context — and it sharpens something I said earlier in this thread.

When I narrowed the gate in a01101b I noted the only run removed was course-v1:MITxT+14.73GZ+1T2025, "which has no proctored exam grades at all, so no mart output changes." That is accurate, and the parity result stands. But I went and checked why it has none, and the answer is a pre-existing bug this PR neither causes nor worsens:

That run has a block that used to be titled proctored exam and is now titled Final Exam, carrying 32 learners with graded attempts. Because int__mitxonline__proctored_exam_grades.exam_unit_grades keys on that exact title string, the rename silently evicted all of them:

check (production, 2026-09-16) result
rows in int__mitxonline__proctored_exam_grades for that run 0
rows in marts__micromasters_dedp_exam_grades for that run 0
mart total 20,867

It is the only affected run today. 14.73 is a DEDP course, so this is probably a genuine reporting gap rather than an artifact — but whether Final Exam is that proctored exam, or the block was legitimately repurposed, is a question for the course team rather than something the warehouse can answer. Flagging rather than guessing.

Tracked as tk-dedp-exam-grades-silently-drops-a-course-run-whe-03c080, with the proposed drift guard: key on block ids that have ever carried the title, flag any that still have graded attempts but no longer carry it. It has to ship as warn initially, since the 14.73GZ case would make an error-severity version land red. It belongs on the intermediate model, not on dim_course_run — the dimension's semester is the symptom, the lost rows are the disease.

@KatelynGit flagging in case the 32 missing learners matter for DEDP reporting independently of this migration.

The migration validation flow in this PR's testing instructions materializes
the pre-migration version of each changed model as a sibling model file
(`<model>_pre.sql`, built from origin/main) so `ol-dbt diff` can compare the
two sides. Those files are throwaway, but they are real dbt models in a
tracked directory, so they show up as untracked and a `git add -A` commits
them.

Verified the pattern catches both baselines this PR uses and shadows no
tracked model (`git ls-files 'src/ol_dbt/models/**/*_pre.sql'` is empty).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quazi-h added a commit that referenced this pull request Sep 16, 2026
…nd state the scope up front

Folds in what two manual validation runs on #2403 and #2686 learned but the
skill did not yet say.

CAUTIOUS DROPS THE PR'S OWN TEST. `--indirect-selection=cautious` includes a
test only when EVERY model it references is selected, so a new singular test
the PR itself adds — referencing the changed model plus something outside the
selection — is silently excluded. On #2403 that was the PR's own new test, the
one test most worth seeing. Switched the recommendation to `buildable`, which
relaxes exactly that condition and still excludes the eager relationships
noise. Re-measured both modes rather than trusting the July figures: on
dim_course_run eager 17 / buildable 8 / cautious 8, and the single test
buildable adds is the course_fk -> dim_course relationship.

RUN THE TESTS BEFORE THE DIFF, with the reason: a named failing test is more
diagnostic than a 20k-row mismatch report, and a broken model makes a MATCH
uninterpretable — you cannot distinguish agreement from two identically-wrong
sides.

WHAT TO EXPECT was missing entirely — the skill said which figures to trust
but never what the report looks like or what a pass is. Step 5(d) now says to
diff EVERY changed model with a per-model verdict written down first:
unaffected -> MATCH/exit 0; intentionally changed -> MISMATCH/exit 1 where the
SHAPE is the assertion (row delta 0, only the intended columns, the intended
direction). So a MISMATCH is not a failure and a MATCH is not automatically a
pass. Includes the #2403 numbers that turned an asserted claim into a measured
one (semester 12 -> 99 non-null over 87 re-versioned rows, body had said
"~100"), and the --exclude-columns effective_date,end_date that SCD2 models
need or the report is unreadable.

Also rewrote the frontmatter description and the opening paragraph, which
undersold the skill as "the acceptance procedure" when it is the whole
register/build/test/diff workflow. Anyone scanning the description could not
tell whether it covered the higher-level sequence or just the diff command.

Two stale spots caught by grepping for what the change touched: "test
separately, cautiously" now misread as the mode name, and a "runs 7 tests"
figure that re-measurement put at 8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quazi-h added a commit that referenced this pull request Sep 16, 2026
Two gaps found by checking the skill against everything the #2403 and #2686
manual runs produced, rather than against the review findings.

NO NEGATIVE CONTROL. The skill said what a clean result looks like but never
how to prove the comparison can fail. A MATCH is unfalsifiable on its own —
it is indistinguishable from a diff that compared nothing. Added the recipe
(replace the column under test with a literal in _pre, rebuild that model
only, rerun) with the #2403 evidence that it does fail loudly: perturbing
semester gave `mismatch — 17888 unmatched row-side(s), 1 column value
mismatch(es)`, exit 1, rows listed. Also says to put the one-liner in the PR
body, since a reviewer cannot otherwise distinguish the two.

THE SHARED DUCKDB CAN STOP YOU, not just mislead you. The section covered
staleness and clobbering but not the hard failure: with another session
holding the file, ol-dbt diff dies on `_duckdb.IOException: Could not set
lock on file ... Conflicting lock is held` — 5 retries at ~20s on #2403.
Called out as contention rather than a defect in the change under review, so
nobody reports it as one.

Renumbered that list's intro from "Two consequences" to "Three", caught by
grepping what the edit touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Populated only for MITxOnline course runs that correspond to a MicroMasters
exam
run. Null for all other platforms and non-exam MITxOnline course runs.
Academic term identifier (e.g. "2T2022") for a course run that carries a

@KatelynGit KatelynGit Sep 17, 2026

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.

These definitions are not great and clearly not written or reviewed by a human

@KatelynGit KatelynGit 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.

  • I got one warning when running a build but otherwise it was clean:

[WARNING]: in test not_null_dim_course_run_course_fk (models/dimensional/_dim_course_run.yml)
[WARNING]: Got 626 results, configured to warn if != 0

  • The definitions could be improved they are definitely not written by a human and don't make sense

  • Was a data diff run against old and new?

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.

3 participants