feat(migration): ICT dimension spine — regions, schools, coaches, teachers, FICO KPIs - #119
Open
hammad-sarfraz-1 wants to merge 3 commits into
Open
Conversation
The ICT observation facts are already in Supabase (migrate-coaching-observations.py moved 10 tables incl. 255k answers), but their FKs dangle: observations.coach_id, teacher_visits.teacher_id and school_visits.school_id have nothing to resolve against. So 'which coach observed which teacher, at which school, in which sector' is answerable at source and unanswerable in Supabase today. This plan closes that gap with five lookup tables (7 sectors, 432 schools, 117 coach profiles, 4,310 teacher profiles, 5,180 FICO KPI rows), a Python migration script following the established fde_production -> nietemigrated_* pattern, and an ORM-style access service over supabase-js. Verified live against source 2026-08-04: 8,460 active observations, 63 coaches, 2,711 teachers observed, 337 schools, span 2025-08-22 -> 2026-08-03. Additive: no existing row written or deleted, no column dropped or retyped. The three FK constraints on already-migrated fact tables are ADD CONSTRAINT NOT VALID (metadata only, cannot reject legacy rows) and are required for PostgREST embedded joins. Rollback is five DROP TABLEs plus three DROP CONSTRAINTs.
fico_kpis migrated 27 of 28 source columns, including cnic, date_of_birth and basic_pay_scale behind a read-layer allow-list. Dropping them outright is stronger: no column to leak, no tier rule to enforce, select(*) safe anywhere. Also drops teacher_name / school / sector / levels / contact_number, which repeat identically on every observation of the same teacher. Those now come from nietemigrated_teacher_profiles via a new nietemigrated_fico_with_teacher view that owns the profile tie-break in one place. Table goes 27 -> 13 columns: observation grain (user_id, date, grade, subject), the six KPIs, total, percentage, plus emis as the join tie-breaker. Tie-break exposure measured live 2026-08-04: 4,259 teachers, 51 multi-profile. FICO covers 2,257 of them, only 9 multi-profile. A further 9 FICO user_ids have no active profile at all, so the view LEFT JOINs and their scores survive with a null teacher name rather than disappearing.
V1.0.5 imported the coaching-observation FACTS but preserved the Django FK columns
as opaque BIGINTs, noting that consumers needing identity 'join via other paths'.
No such path existed: no region, school, coach-profile or teacher-profile table was
ever migrated, so which-coach-observed-which-teacher-where was unanswerable.
Adds:
infrastructure/supabase/migrations/V1.0.11__ict_dimension_spine.sql
5 tables (school_regions 7, schools 462, coach_profiles 117,
teacher_profiles 4310, fico_kpis 5180) + a fico_with_teacher view + RLS
+ 3 guarded NOT VALID FKs that resolve V1.0.5's opaque coach_id /
teacher_id / school_id columns.
scripts/migrate-ict-spine.py
Loader following the fde_production -> nietemigrated_* house pattern:
governed SELECTs, --dry-run default, --commit, --tables, --verify.
Validated against PostgreSQL 16 on 2026-08-04 with V1.0.5-shaped fact tables
(UUID PKs, BIGINT opaque FKs). 10 assertions pass: clean apply, idempotent
re-apply, all 3 FKs attach as NOT VALID, new bad writes rejected while legacy
rows tolerated, view does not fan out on multi-profile teachers, EMIS match
beats recency in the tie-break, observation grain respected across subjects,
profile-less scores survive with a null name, sector-less schools resolve,
zero HR columns present. DOWN block drops the spine and leaves V1.0.5 intact.
fico_kpis takes 13 of 28 source columns. The 9 HR columns (cnic, date_of_birth,
gender, joining_date, last_promotion_date, qualifications, professional_trainings,
service_designation, basic_pay_scale) are never migrated — no column to leak beats
a read-side allow-list. teacher_name/school/sector/levels also dropped: they repeat
per observation and come from teacher_profiles via the view.
Corrects the plan's school count: 462 live rows, not 432 (the earlier figure
omitted the deleted_at filter). 459 distinct EMIS, 3 NULL, so emis is not unique.
tests/setup guard suite: 20 failures before and after — zero regressions, and no
failure references any new object.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Makes the migrated ICT coaching observations attributable: which coach observed which teacher, at which school, in which sector.
The gap
V1.0.5__coaching_observations_migrated.sqlimported the observation facts — 9,944 observations, 255,417 answers, 497,177 question options, 8,973 teacher visits. It preserved the Django FK columns as opaque BIGINTs, noting that consumers needing identity should "join via other paths".There is no such path. No region, school, coach-profile or teacher-profile table was ever migrated:
So the question is answerable at source and unanswerable in Supabase.
What this adds
V1.0.11__ict_dimension_spine.sql— five lookup tables, one view, RLS, and three guarded FK back-fills that resolve V1.0.5's opaque columns:nietemigrated_school_regionsnietemigrated_schoolsnietemigrated_coach_profilesnietemigrated_teacher_profilesnietemigrated_fico_kpisscripts/migrate-ict-spine.py— the loader, following the establishedfde_production→nietemigrated_*pattern used by the 9 existingmigrate-*.pyscripts.--dry-runby default, plus--commit,--tables,--verify.Verified live at source, 2026-08-04
Validated against real PostgreSQL 16
Run with V1.0.5-shaped fact tables (UUID PKs, BIGINT opaque FKs) — not a syntax check. 10 assertions pass: clean apply · idempotent re-apply · all 3 FKs attach as
NOT VALID· a new bad write is rejected while legacy rows are tolerated · the view does not fan out on multi-profile teachers · EMIS match beats recency in the tie-break · observation grain respected across subjects · profile-less scores survive with a null name · sector-less schools resolve · zero HR columns present.The DOWN block drops the spine and leaves V1.0.5 untouched.
Conventions followed
infrastructure/supabase/migrations/asV1.0.11__, matching V1.0.5 (the siblingnietemigrated_*migration).source_system+migrated_atprovenance columns, as V1.0.5 carries.fde_productionviaTALEEMABAD_DB_*. No BigQuery dependency.PII: not migrated, rather than guarded
fico_kpistakes 13 of 28 source columns. The 9 HR columns — cnic, date_of_birth, gender, joining_date, last_promotion_date, qualifications, professional_trainings, service_designation, basic_pay_scale — are never selected. No column to leak beats a read-side allow-list.Also dropped:
teacher_name/school/sector/levels/contact_number, which repeat identically on every observation of the same teacher. They come fromteacher_profilesthrough the view.The tie-break, and why a view
fico_kpislinks byuser_id;teacher_profilesis keyed by profile id, and one user can hold two profiles (a transferred teacher). The view owns the rule once: prefer the profile whose school EMIS matches the observation, else the most recently modified.Measured, so the exposure is known: 4,259 teachers, 51 multi-profile. FICO covers 2,257, of which 9 are multi-profile. A further 9 FICO user_ids have no active profile at all — the view LEFT JOINs so their scores survive.
Notes for review
emisis deliberately not unique: 462 live schools carry 459 distinct EMIS with 3 NULL, so a unique constraint would reject the load.schools_school462 ·FDE_Schools341 ·Middle_High_Schools_Updated228). Reconciling them is deliberately left as a follow-up rather than silently picking one.V1.0.11; the companion coaching PR addsV1.0.12. Merging this first keeps the sequence gapless.