Skip to content

feat(migration): ICT dimension spine — regions, schools, coaches, teachers, FICO KPIs - #119

Open
hammad-sarfraz-1 wants to merge 3 commits into
Orenda-Project:mainfrom
hammad-sarfraz-1:feat/ict-dimension-spine
Open

feat(migration): ICT dimension spine — regions, schools, coaches, teachers, FICO KPIs#119
hammad-sarfraz-1 wants to merge 3 commits into
Orenda-Project:mainfrom
hammad-sarfraz-1:feat/ict-dimension-spine

Conversation

@hammad-sarfraz-1

Copy link
Copy Markdown

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.sql imported 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:

nietemigrated_observations.coach_id     BIGINT  -> nothing
nietemigrated_teacher_visits.teacher_id BIGINT  -> nothing
nietemigrated_school_visits.school_id   BIGINT  -> nothing

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:

Table Rows
nietemigrated_school_regions 7 sectors
nietemigrated_schools 462
nietemigrated_coach_profiles 117
nietemigrated_teacher_profiles 4,310 rows / 4,259 people
nietemigrated_fico_kpis 5,180

scripts/migrate-ict-spine.py — the loader, following the established fde_productionnietemigrated_* pattern used by the 9 existing migrate-*.py scripts. --dry-run by default, plus --commit, --tables, --verify.

Verified live at source, 2026-08-04

  • 8,460 active observations, test accounts excluded (6,406 coach / 900 principal / 1,154 teacher-self)
  • 63 coach profiles · 2,711 teachers observed · 337 schools
  • Span 2025-08-22 → 2026-08-03
  • Sectors: B.K 84 · Sihala 79 · Urban-I 74 · Nilore 67 · Urban-II 67 · Tarnol 61 · (no region) 32 · Durbeen 1

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/ as V1.0.11__, matching V1.0.5 (the sibling nietemigrated_* migration).
  • source_system + migrated_at provenance columns, as V1.0.5 carries.
  • BIGINT PKs to match V1.0.5's opaque FK columns exactly — no ID remapping.
  • Reads fde_production via TALEEMABAD_DB_*. No BigQuery dependency.

PII: not migrated, rather than guarded

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 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 from teacher_profiles through the view.

The tie-break, and why a view

fico_kpis links by user_id; teacher_profiles is 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

  • Nothing has been applied to Supabase and the loader has not been run. This is schema + script only.
  • emis is deliberately not unique: 462 live schools carry 459 distinct EMIS with 3 NULL, so a unique constraint would reject the load.
  • 32 schools have no region and are unfilterable by sector — surfaced, not hidden.
  • Three school lists disagree (schools_school 462 · FDE_Schools 341 · Middle_High_Schools_Updated 228). Reconciling them is deliberately left as a follow-up rather than silently picking one.
  • Contains V1.0.11; the companion coaching PR adds V1.0.12. Merging this first keeps the sequence gapless.

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

1 participant