feat(b2b_learner_records): add a contract x course-run MV for /courses - #2684
blarghmatey wants to merge 3 commits into
Conversation
🔎 ol-dbt impact — column-level blast radius0 breaking, 0 surrogate-key regeneration, 0 warning, 1 info across 1 changed model(s). Details
Posted by |
There was a problem hiding this comment.
🟡 Changes recommended
Unlimited seat limits are misrepresented and the PII views remain exposed through catalog-wide grants.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds StarRocks materialized views supporting the B2B learner-records API, including its /courses endpoint.
Changes:
- Adds learner, enrollment, and contract/course-run materialized views.
- Adds organization-membership history via a dbt snapshot.
- Configures and documents the
b2b_learner_recordsschema.
File summaries
| File | Description |
|---|---|
src/ol_dbt/snapshots/_mitxonline__snapshots.yml |
Defines membership-history snapshot. |
src/ol_dbt/models/b2b_learner_records/mv_b2b_learner.sql |
Adds organization-level learner records. |
src/ol_dbt/models/b2b_learner_records/mv_b2b_learner_enrollment.sql |
Adds learner enrollment records. |
src/ol_dbt/models/b2b_learner_records/mv_b2b_contract_courserun.sql |
Adds /courses backing data. |
src/ol_dbt/models/b2b_learner_records/_b2b_learner_records__models.yml |
Documents and tests the views. |
src/ol_dbt/models/b2b_analytics/_b2b_analytics__sources.yml |
Declares additional dimensional sources. |
src/ol_dbt/dbt_project.yml |
Configures snapshots and the new StarRocks schema. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 2
- Review effort level: Balanced (auto)
Note
Copilot is running an experiment and ran this review at Balanced.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The learner-records API's /courses collection lists every course run under an organization's contracts. mv_b2b_learner_enrollment can't back it: it inner-joins tfact_enrollment, so a run nobody has enrolled in is missing, and it carries none of the contract status, dates or seat limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EDJRuj6mDpGRqNbQSsXZ9o
MITx Online's ContractPage.max_learners help text documents both 0 and blank as unlimited, and it creates its retirement holding contract with max_learners=0. The learner-records API contract reserves null for uncapped, so passing 0 through would tell a partner the contract has no seats. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EDJRuj6mDpGRqNbQSsXZ9o
d92bead to
87cd273
Compare
quazi-h
left a comment
There was a problem hiding this comment.
Reviewed at 87cd2730. Two findings, both about what the columns mean to the
API rather than about the SQL — the joins and the grain measure clean.
Heads up on one process thing: Copilot's review is against 91f48bba, which the
rebase orphaned, so nothing has bot-reviewed the current head.
What I verified by running this model's exact SELECT against the production
dimensional Iceberg tables in DuckDB:
- Grain holds. 2,770 rows, 2,770 distinct
(contract_pk, courserun_pk), and
2,770 distinct(contract_id, courserun_readable_id)— so the spec's "ordered
by(contract_id, courserun_id), which is unique" is true of the API's own key
and not just the surrogate version. 2,770 distinctcourserun_pk, i.e. no run
appears under two contracts, which is theb2b_contract_id-is-a-single-FK
claim measured rather than asserted. - No join drops anything. bridge 2,770 → after
dim_contract2,770 → after
dim_organization2,770 → afterplatform = 'mitxonline'2,770 → after
is_current2,770. All 74 contracts have a non-nullorganization_fk, so the
inner join todim_organizationcosts nothing today, andboc.organization_fk
andc.organization_fkagree on all 2,770 rows. is_current = trueis load-bearing, not decorative. 89 of the runs in this
view have anis_current = falsetwin indim_course_run(2,378 historical
rows overall), so dropping that predicate would fan those 89 out.- Types line up with the spec — including the end-date pair you flagged as
unchecked:b2b_contract_end_dateisDATE(specdate),contract_id
BIGINT(specinteger/int64),seat_limitBIGINT.courserun_end_onis a
VARCHAR of the same shape ascourserun_start_on, which is the first finding. - No nulls in the spec's non-nullable fields: 0 null across
organization_name,organization_key,contract_id,b2b_contract_name,
b2b_contract_is_active,courserun_readable_id,courserun_title. All 736
non-nullsso_organization_idvalues are UUID-shaped. - "Null for self-paced runs" checks out. I went at this one expecting it to
fail, sincecourserun_end_onis null on 2,752 of 2,770 rows and 99%
self-paced seemed unlikely. It isn't:is_self_pacedis true for 2,703 of the
2,770 runs, and 2,689 of the 2,752 nulls are self-paced runs. - "No Dagster change" checks out.
_materialized_view_nodesselects on
materialized == 'materialized_view'plus thestarrockstag, both set by the
directory config, and the schema comes from the manifest node
(lib/starrocks_dbt.py:97,122), sob2b_learner_records.mv_b2b_contract_courserun
is picked up with no Python edit. The yml documents all 15 columns in SELECT
order, which the drift check's set-equality needs, andvalidate's
yaml_sql_syncstill errors in both directions here because the model has no
SELECT *to defeat sqlglot. ol-dbt validate: 0 errors; the new model's only warnings are the four
unresolvable-source()column lists, the same class both merged siblings get.
Not raising, since you disclosed both and I measured the same numbers: the null
contract_start_date / contract_end_date (0 of 2,770 start, 1 of 2,770 end)
and the 2,034 rows with no sso_organization_id (736 reachable rows, 25
contracts, 21 orgs).
The first finding would normally be request-changes. Posting as a comment so you
can decide which layer owns it, or push back.
…nd the active flag's limits The API is written against these descriptions. courserun_start_on/end_on carry UTC with no offset (Trino's cast_timestamp_to_iso8601 drops the zone), which ol-analytics-api's UtcDatetime normalizes. b2b_contract_is_active is a manual MITx Online flag that ignores the end date. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWD7gHumQjFi9FWM5HXkqE
What are the relevant tickets?
N/A. Backs
GET /organizations/{organization_id}/coursesin the learner-records spec (mitodl/ol-analytics-api#55). The endpoint is mitodl/ol-analytics-api#60. Stacked on #2669, so the base isfeat/b2b-learner-grain-mvs.Description (What does it do?)
Adds
mv_b2b_contract_courseruntomodels/b2b_learner_records/, grain org × contract × course run. It joinsbridge_organization_courserun,dim_contract,dim_organizationanddim_course_run(platform = 'mitxonline',is_current), and carries the contract's active flag, start and end dates andseat_limitnext to the run identifiers.seat_limitisb2b_contract_max_learnerswith 0 folded into null: MITx Online documents both 0 and blank as unlimited, and the API contract reserves null for uncapped.mv_b2b_learner_enrollmentcan't back/courses. It inner-joinstfact_enrollment, so a run nobody has enrolled in drops out, and it has no contract status, dates or seat limit. The new view holds no personal data. It lives inb2b_learner_recordsso the tenant reads one database.No Dagster change: the refresh asset and the drift check find MVs by the
starrockstag in the manifest, which the directory config already sets.How can this be tested?
ol-dbt validate: 0 errors. The new model's only warnings are the unresolvablesource()column lists, whichmv_b2b_learnergets too. Pre-commit (sqlfluff, yamllint) passes.contract_pk,courserun_pk), across 73 contracts.tfact_enrollment, so a view derived frommv_b2b_learner_enrollmentwould miss them.b2b_contract_start_dateis DATE andcourserun_start_onan ISO-8601 string, matching the spec'sdateanddate-time. I didn't check the end-date columns' types separately.starrocks_dbt_assetsrun creates it. Confirm withDESC b2b_learner_records.mv_b2b_contract_courserun.Additional Context
What the API will return from this data today:
contract_start_dateis null everywhere.dim_contract.b2b_contract_start_dateis null for all 74 production contracts, andcontract_startis null in all 74 rows of the rawb2b_contractpagetable, so the dbt models aren't dropping it.b2b_contract_end_dateis null for 73 of the 74.seat_limitis null (uncapped) for 27 of 74 contracts.sso_organization_id. It's the same gap feat(b2b_analytics): add learner-grain MVs for the learner-records API #2669 reports: the API filters on it, so those organizations' runs aren't reachable.Checklist:
main🤖 Generated with Claude Code
https://claude.ai/code/session_01EDJRuj6mDpGRqNbQSsXZ9o