Skip to content

feat(b2b_learner_records): add a contract x course-run MV for /courses - #2684

Open
blarghmatey wants to merge 3 commits into
mainfrom
feat/b2b-learner-records-contract-courserun-mv
Open

blarghmatey wants to merge 3 commits into
mainfrom
feat/b2b-learner-records-contract-courserun-mv

Conversation

@blarghmatey

@blarghmatey blarghmatey commented Sep 15, 2026

Copy link
Copy Markdown
Member

What are the relevant tickets?

N/A. Backs GET /organizations/{organization_id}/courses in the learner-records spec (mitodl/ol-analytics-api#55). The endpoint is mitodl/ol-analytics-api#60. Stacked on #2669, so the base is feat/b2b-learner-grain-mvs.

Description (What does it do?)

Adds mv_b2b_contract_courserun to models/b2b_learner_records/, grain org × contract × course run. It joins bridge_organization_courserun, dim_contract, dim_organization and dim_course_run (platform = 'mitxonline', is_current), and carries the contract's active flag, start and end dates and seat_limit next to the run identifiers. seat_limit is b2b_contract_max_learners with 0 folded into null: MITx Online documents both 0 and blank as unlimited, and the API contract reserves null for uncapped.

mv_b2b_learner_enrollment can't back /courses. It inner-joins tfact_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 in b2b_learner_records so the tenant reads one database.

No Dagster change: the refresh asset and the drift check find MVs by the starrocks tag 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 unresolvable source() column lists, which mv_b2b_learner gets too. Pre-commit (sqlfluff, yamllint) passes.
  • I ran the model's joins and filters against production dimensional Iceberg tables in DuckDB, re-registered 2026-09-15. It hasn't been built on StarRocks.
    • 2,770 rows, unique on (contract_pk, courserun_pk), across 73 contracts.
    • 1,577 of those runs have no row in tfact_enrollment, so a view derived from mv_b2b_learner_enrollment would miss them.
    • b2b_contract_start_date is DATE and courserun_start_on an ISO-8601 string, matching the spec's date and date-time. I didn't check the end-date columns' types separately.
  • After feat(b2b_analytics): add learner-grain MVs for the learner-records API #2669 and this merge, the next starrocks_dbt_assets run creates it. Confirm with DESC b2b_learner_records.mv_b2b_contract_courserun.

Additional Context

What the API will return from this data today:

  • contract_start_date is null everywhere. dim_contract.b2b_contract_start_date is null for all 74 production contracts, and contract_start is null in all 74 rows of the raw b2b_contractpage table, so the dbt models aren't dropping it. b2b_contract_end_date is null for 73 of the 74.
  • seat_limit is null (uncapped) for 27 of 74 contracts.
  • 2,034 of the 2,770 rows have a null 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:

🤖 Generated with Claude Code

https://claude.ai/code/session_01EDJRuj6mDpGRqNbQSsXZ9o

@github-actions

Copy link
Copy Markdown

🔎 ol-dbt impact — column-level blast radius

0 breaking, 0 surrogate-key regeneration, 0 warning, 1 info across 1 changed model(s).

Details
  • ℹ️ mv_b2b_contract_courserunb2b_contract_end_date, b2b_contract_is_active, b2b_contract_name, b2b_contract_start_date, contract_id, contract_pk, courserun_end_on, courserun_pk, courserun_readable_id, courserun_start_on, courserun_title, organization_key, organization_name, seat_limit, sso_organization_id → 0 downstream model(s)

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

Comment thread src/ol_dbt/models/b2b_learner_records/_b2b_learner_records__models.yml Outdated
@blarghmatey
blarghmatey requested a balanced review from Copilot September 15, 2026 19:31
Base automatically changed from feat/b2b-learner-grain-mvs to main September 15, 2026 19:32
@blarghmatey
blarghmatey requested a review from quazi-h September 15, 2026 19:32

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

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_records schema.
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.

Comment thread src/ol_dbt/dbt_project.yml
Comment thread src/ol_dbt/models/b2b_learner_records/mv_b2b_contract_courserun.sql Outdated
blarghmatey and others added 2 commits September 15, 2026 15:55
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
@blarghmatey
blarghmatey force-pushed the feat/b2b-learner-records-contract-courserun-mv branch from d92bead to 87cd273 Compare September 15, 2026 19:55

@quazi-h quazi-h 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.

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 distinct courserun_pk, i.e. no run
    appears under two contracts, which is the b2b_contract_id-is-a-single-FK
    claim measured rather than asserted.
  • No join drops anything. bridge 2,770 → after dim_contract 2,770 → after
    dim_organization 2,770 → after platform = 'mitxonline' 2,770 → after
    is_current 2,770. All 74 contracts have a non-null organization_fk, so the
    inner join to dim_organization costs nothing today, and boc.organization_fk
    and c.organization_fk agree on all 2,770 rows.
  • is_current = true is load-bearing, not decorative. 89 of the runs in this
    view have an is_current = false twin in dim_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_date is DATE (spec date), contract_id
    BIGINT (spec integer/int64), seat_limit BIGINT. courserun_end_on is a
    VARCHAR of the same shape as courserun_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-null sso_organization_id values are UUID-shaped.
  • "Null for self-paced runs" checks out. I went at this one expecting it to
    fail, since courserun_end_on is null on 2,752 of 2,770 rows and 99%
    self-paced seemed unlikely. It isn't: is_self_paced is 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_nodes selects on
    materialized == 'materialized_view' plus the starrocks tag, both set by the
    directory config, and the schema comes from the manifest node
    (lib/starrocks_dbt.py:97,122), so b2b_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, and validate's
    yaml_sql_sync still 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.

Comment thread src/ol_dbt/models/b2b_learner_records/mv_b2b_contract_courserun.sql
Comment thread src/ol_dbt/models/b2b_learner_records/_b2b_learner_records__models.yml Outdated
…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
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