-
Notifications
You must be signed in to change notification settings - Fork 6
feat(user-account-merge): add account merge tracking and edX/MIT Learn user mapping #2474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rachellougee
wants to merge
2
commits into
main
Choose a base branch
from
feat/user-account-merge-mapping
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
src/ol_dbt/models/dimensional/bridge_user_account_link.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| {{ config( | ||
| materialized='table' | ||
| ) }} | ||
|
|
||
| -- Edges between platform accounts, independent of dim_user's own identity- | ||
| -- collapsing (grouping by hashed email). from_platform = to_platform is a | ||
| -- merge redirect (old account -> canonical, confirmed via mitxonline audit | ||
| -- history); different platforms is a co-reference, not a redirect. | ||
| -- Grain: (from_platform, from_user_id, to_platform). | ||
| with courserunenrollment_audit as ( | ||
| select * from {{ ref('stg__mitxonline__app__postgres__courses_courserunenrollmentaudit') }} | ||
| ) | ||
|
|
||
| , programenrollment_audit as ( | ||
| select * from {{ ref('stg__mitxonline__app__postgres__courses_programenrollmentaudit') }} | ||
| ) | ||
|
|
||
| , user_changes as ( | ||
| select | ||
| json_extract_scalar(enrollmentaudit_data_before, '$.user') as platform_user_id_before | ||
| , json_extract_scalar(enrollmentaudit_data_after, '$.user') as platform_user_id_after | ||
| , enrollmentaudit_created_on | ||
| from courserunenrollment_audit | ||
| where json_extract_scalar(enrollmentaudit_data_before, '$.user') | ||
| != json_extract_scalar(enrollmentaudit_data_after, '$.user') | ||
|
|
||
| union all | ||
|
|
||
| select | ||
| json_extract_scalar(enrollmentaudit_data_before, '$.user') as platform_user_id_before | ||
| , json_extract_scalar(enrollmentaudit_data_after, '$.user') as platform_user_id_after | ||
| , enrollmentaudit_created_on | ||
| from programenrollment_audit | ||
| where json_extract_scalar(enrollmentaudit_data_before, '$.user') | ||
| != json_extract_scalar(enrollmentaudit_data_after, '$.user') | ||
| ) | ||
|
|
||
| , grouped as ( | ||
| select | ||
| cast(platform_user_id_before as integer) as platform_user_id_before | ||
| , cast(platform_user_id_after as integer) as platform_user_id_after | ||
| , max(enrollmentaudit_created_on) as last_merged_on | ||
| from user_changes | ||
| group by platform_user_id_before, platform_user_id_after | ||
| ) | ||
|
|
||
| -- Normalize to an unordered pair so a reversed merge (both directions | ||
| -- present) competes against itself, keeping only the most recent direction. | ||
| -- Note: QUALIFY is not supported by Trino; using ROW_NUMBER subquery. | ||
| , ranked_merges as ( | ||
| select | ||
| * | ||
| , row_number() over ( | ||
| partition by | ||
| least(platform_user_id_before, platform_user_id_after) | ||
| , greatest(platform_user_id_before, platform_user_id_after) | ||
| order by last_merged_on desc | ||
| ) as row_num | ||
| from grouped | ||
| ) | ||
|
|
||
| , account_merges as ( | ||
| select | ||
| 'mitxonline' as from_platform | ||
| , platform_user_id_before as from_user_id | ||
| , 'mitxonline' as to_platform | ||
| , platform_user_id_after as to_user_id | ||
| , last_merged_on as observed_on | ||
| from ranked_merges | ||
| where row_num = 1 | ||
| ) | ||
|
|
||
| , platform_links as ( | ||
| select | ||
| 'mitxonline' as from_platform | ||
| , user_mitxonline_id as from_user_id | ||
| , 'edxorg' as to_platform | ||
| , user_edxorg_id as to_user_id | ||
| , cast(null as varchar) as observed_on | ||
| from {{ ref('int__mitx__users') }} | ||
| where user_mitxonline_id is not null | ||
| and user_edxorg_id is not null | ||
| ) | ||
|
|
||
| select * from account_merges | ||
| union all | ||
| select * from platform_links |
68 changes: 68 additions & 0 deletions
68
src/ol_dbt/models/dimensional/bridge_user_account_merge.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| {{ config( | ||
| materialized='table' | ||
| ) }} | ||
|
|
||
| -- Verified account-merge redirects derived from mitxonline audit history | ||
| -- (an admin reassigning a record's owner confirms two accounts are the same | ||
| -- person). Only mitxonline today; `platform` allows adding others later. | ||
| -- Grain: one row per (platform, platform_user_id_before). A merge can be | ||
| -- reversed later, so only the most recent direction per account pair is kept. | ||
| with courserunenrollment_audit as ( | ||
| select * from {{ ref('stg__mitxonline__app__postgres__courses_courserunenrollmentaudit') }} | ||
| ) | ||
|
|
||
| , programenrollment_audit as ( | ||
| select * from {{ ref('stg__mitxonline__app__postgres__courses_programenrollmentaudit') }} | ||
| ) | ||
|
|
||
| , user_changes as ( | ||
| select | ||
| json_extract_scalar(enrollmentaudit_data_before, '$.user') as platform_user_id_before | ||
| , json_extract_scalar(enrollmentaudit_data_after, '$.user') as platform_user_id_after | ||
| , enrollmentaudit_created_on | ||
| from courserunenrollment_audit | ||
| where json_extract_scalar(enrollmentaudit_data_before, '$.user') | ||
| != json_extract_scalar(enrollmentaudit_data_after, '$.user') | ||
|
|
||
| union all | ||
|
|
||
| select | ||
| json_extract_scalar(enrollmentaudit_data_before, '$.user') as platform_user_id_before | ||
| , json_extract_scalar(enrollmentaudit_data_after, '$.user') as platform_user_id_after | ||
| , enrollmentaudit_created_on | ||
| from programenrollment_audit | ||
| where json_extract_scalar(enrollmentaudit_data_before, '$.user') | ||
| != json_extract_scalar(enrollmentaudit_data_after, '$.user') | ||
| ) | ||
|
|
||
| , grouped as ( | ||
| select | ||
| cast(platform_user_id_before as integer) as platform_user_id_before | ||
| , cast(platform_user_id_after as integer) as platform_user_id_after | ||
| , max(enrollmentaudit_created_on) as last_merged_on | ||
| from user_changes | ||
| group by platform_user_id_before, platform_user_id_after | ||
| ) | ||
|
|
||
| -- Normalize to an unordered pair so a reversed merge (both directions | ||
| -- present) competes against itself, keeping only the most recent direction. | ||
| -- Note: QUALIFY is not supported by Trino; using ROW_NUMBER subquery. | ||
| , ranked as ( | ||
| select | ||
| * | ||
| , row_number() over ( | ||
| partition by | ||
| least(platform_user_id_before, platform_user_id_after) | ||
| , greatest(platform_user_id_before, platform_user_id_after) | ||
| order by last_merged_on desc | ||
| ) as row_num | ||
| from grouped | ||
| ) | ||
|
|
||
| select | ||
| 'mitxonline' as platform | ||
| , platform_user_id_before | ||
| , platform_user_id_after | ||
| , last_merged_on as merged_on | ||
| from ranked | ||
| where row_num = 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
src/ol_dbt/models/reporting/edxorg_mitlearn_user_mapping.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| -- Referencing bridge_user_account_link (not just dim_user) is intentional: | ||
| -- dim_user can fold unrelated accounts together when their computed emails | ||
| -- collide, and its own output is the thing being validated, so it can't be | ||
| -- used to catch its own collisions -- the bridge has the pre-collapse link. | ||
| with mitxonline_edxorg_link as ( | ||
| select from_user_id as mitxonline_application_user_id, to_user_id as user_edxorg_id | ||
| from {{ ref('bridge_user_account_link') }} | ||
| where from_platform = 'mitxonline' and to_platform = 'edxorg' | ||
| ) | ||
|
|
||
| , users as ( | ||
| select | ||
| dim_user.mitxonline_application_user_id | ||
| , dim_user.mitxonline_openedx_user_id | ||
| , case | ||
| when mitxonline_edxorg_link.user_edxorg_id = dim_user.edxorg_openedx_user_id | ||
| then dim_user.edxorg_openedx_user_id | ||
| end as edxorg_user_id | ||
| , dim_user.mitlearn_user_id as mitlearn_application_user_id | ||
| from {{ ref('dim_user') }} as dim_user | ||
| left join mitxonline_edxorg_link | ||
| on dim_user.mitxonline_application_user_id = mitxonline_edxorg_link.mitxonline_application_user_id | ||
| where dim_user.mitxonline_application_user_id is not null | ||
| ) | ||
|
|
||
| , merges as ( | ||
| select from_user_id as platform_user_id_before, to_user_id as platform_user_id_after | ||
| from {{ ref('bridge_user_account_link') }} | ||
| where from_platform = 'mitxonline' and to_platform = 'mitxonline' | ||
| ) | ||
|
|
||
| -- A canonical account can have more than one account merged into it, so | ||
| -- aggregate per canonical account. max() only stands in for coalesce here -- | ||
| -- the count(distinct ...) guard nulls the result out instead of guessing if | ||
| -- two merged-away accounts carry genuinely conflicting non-null values. | ||
| , merged_away_links as ( | ||
| select | ||
| merges.platform_user_id_after as mitxonline_user_id | ||
| , case | ||
| when count(distinct users.edxorg_user_id) <= 1 | ||
| then max(users.edxorg_user_id) | ||
| end as edxorg_user_id | ||
| , case | ||
| when count(distinct users.mitlearn_application_user_id) <= 1 | ||
| then max(users.mitlearn_application_user_id) | ||
| end as mitlearn_application_user_id | ||
| from merges | ||
| inner join users | ||
| on merges.platform_user_id_before = users.mitxonline_application_user_id | ||
| group by merges.platform_user_id_after | ||
| ) | ||
|
|
||
| , mapped as ( | ||
| select | ||
| users.mitxonline_openedx_user_id | ||
| , coalesce(users.edxorg_user_id, merged_away_links.edxorg_user_id) as edxorg_user_id | ||
| , coalesce(users.mitlearn_application_user_id, merged_away_links.mitlearn_application_user_id) | ||
| as mitlearn_application_user_id | ||
| from users | ||
| left join merged_away_links | ||
| on users.mitxonline_application_user_id = merged_away_links.mitxonline_user_id | ||
| where users.mitxonline_application_user_id not in (select platform_user_id_before from merges) | ||
| ) | ||
|
|
||
| select mitxonline_openedx_user_id, edxorg_user_id, mitlearn_application_user_id | ||
| from mapped | ||
| where edxorg_user_id is not null or mitlearn_application_user_id is not null | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.