Files
school_compare/pipeline/transform/models/marts/dim_school.sql
T
Tudor Sitaru ca5f6a962c
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 45s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m10s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
fix(dim_school): expand phase inference with name-based fallback
The case-insensitive "Not Applicable" fix caught schools where GIAS publishes
statutory ages, but some independent schools leave those blank too — they fall
through every branch and end up with null phase and no pill in the UI.

Add a third tier that infers phase from the school name
(Primary/Infant/Junior/Prep vs Secondary/High/Grammar/Senior/Upper) and also
normalise "Not Applicable" handling with trim() + "unknown"/"" exclusion, so
the final else branch can safely return null instead of the catch-all string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 21:15:54 +01:00

77 lines
2.7 KiB
SQL

-- Mart: Canonical school dimension — one row per active URN
with schools as (
select * from {{ ref('stg_gias_establishments') }}
)
{% set ofsted_relation = adapter.get_relation(
database=target.database,
schema=target.schema,
identifier='int_ofsted_latest'
) %}
select
s.urn,
s.local_authority_code * 1000 + s.establishment_number as laestab,
s.school_name,
case
-- 1. Trust GIAS phase when it's a real value (not the catch-all "Not Applicable")
when s.phase is not null
and lower(trim(s.phase)) not in ('not applicable', '', 'unknown')
then s.phase
-- 2. Infer from statutory age range (independent schools still publish these)
when s.statutory_high_age is not null and s.statutory_high_age <= 11 then 'Primary'
when s.statutory_low_age is not null and s.statutory_low_age >= 11 then 'Secondary'
when s.statutory_low_age is not null and s.statutory_high_age is not null
and s.statutory_low_age < 11 and s.statutory_high_age > 11 then 'All-through'
-- 3. Fallback: infer from school name (covers independents with missing ages)
when s.school_name ilike '%primary%'
or s.school_name ilike '%infant%'
or s.school_name ilike '%junior%'
or s.school_name ilike '%preparatory%'
or s.school_name ilike '% prep school%'
or s.school_name ilike '% prep %'
then 'Primary'
when s.school_name ilike '%secondary%'
or s.school_name ilike '%high school%'
or s.school_name ilike '%grammar%'
or s.school_name ilike '%senior school%'
or s.school_name ilike '%upper school%'
then 'Secondary'
-- 4. Give up — leave phase null so the UI renders no pill
else null
end as phase,
s.school_type,
s.academy_trust_name,
s.academy_trust_uid,
s.religious_character,
s.gender,
s.statutory_low_age || '-' || s.statutory_high_age as age_range,
s.capacity,
s.total_pupils,
concat_ws(' ', s.head_title, s.head_first_name, s.head_last_name) as headteacher_name,
s.website,
s.telephone,
s.open_date,
s.close_date,
s.status,
s.nursery_provision,
s.admissions_policy,
-- Latest Ofsted (populated after monthly Ofsted pipeline runs)
{% if ofsted_relation is not none %}
o.overall_effectiveness as ofsted_grade,
o.inspection_date as ofsted_date,
o.framework as ofsted_framework
{% else %}
null::text as ofsted_grade,
null::date as ofsted_date,
null::text as ofsted_framework
{% endif %}
from schools s
{% if ofsted_relation is not none %}
left join {{ ref('int_ofsted_latest') }} o on s.urn = o.urn
{% endif %}
where s.status = 'Open'