diff --git a/sql/138-transceiver-speed-drift-guard.sql b/sql/138-transceiver-speed-drift-guard.sql new file mode 100644 index 0000000..18b8439 --- /dev/null +++ b/sql/138-transceiver-speed-drift-guard.sql @@ -0,0 +1,83 @@ +-- sql/138-transceiver-speed-drift-guard.sql +-- Closes a gap in the sql/125+126 "no physically-impossible compatibility rows" +-- invariant: compat_speed_guard() (126) only fires on INSERT/UPDATE of the +-- `compatibility` table itself. It never re-validates existing 'compatible' +-- spec_match rows when a TRANSCEIVER's speed_gbps changes later (e.g. the daily +-- enrich-modulation-fec.sh self-heal, sql/135, recovering speed from part-number +-- tokens for previously NULL/junk rows). Found live 2026-07-12: 189 compat rows +-- had gone physically-impossible because their transceiver's speed_gbps was +-- updated (all sampled rows dated today, i.e. by the 03:30 cron) after the +-- compat row was already written as 'compatible'. +-- +-- Fix mirrors 126 exactly, just triggered from the other table: AFTER UPDATE OF +-- speed_gbps on transceivers, re-validate every 'compatible' spec_match +-- compatibility row referencing that transceiver against the same evidence-based +-- ceiling (v_switch_effective_max) used by v_compat_speed_errors. Curated rows +-- (vendor_matrix/vendor_compat/tested/datasheet) are never touched. Idempotent, +-- safe to re-run. + +-- Backup of affected rows before remediation (audit trail, matches +-- tip_speed_gbps_rollback precedent from sql/136). +create table if not exists compat_speed_drift_backup_20260712 as +with em as materialized ( + select switch_id, effective_max_gbps from v_switch_effective_max) +select c.*, e.effective_max_gbps, t.speed_gbps as tx_speed_gbps, now() as backed_up_at +from compatibility c +join em e on e.switch_id = c.switch_id +join transceivers t on t.id = c.transceiver_id +where c.status = 'compatible' + and c.verification_method = 'spec_match' + and e.effective_max_gbps > 0 + and t.speed_gbps is not null + and t.speed_gbps > e.effective_max_gbps; + +-- One-time remediation: flip the rows found live today (same logic as 125's +-- Remediation B, using the full evidence-based ceiling incl. curated bump). +with em as materialized ( + select switch_id, effective_max_gbps from v_switch_effective_max) +update compatibility c + set status = 'incompatible', + notes = case when c.notes is null or c.notes = '' + then 'auto(138): transceiver speed exceeds switch max port speed (post-hoc drift)' + else c.notes || ' | auto(138): transceiver speed exceeds switch max port speed (post-hoc drift)' end + from em e, transceivers t + where e.switch_id = c.switch_id + and t.id = c.transceiver_id + and c.status = 'compatible' + and c.verification_method = 'spec_match' + and e.effective_max_gbps > 0 + and t.speed_gbps is not null + and t.speed_gbps > e.effective_max_gbps; + +-- Durable fix: re-validate on transceiver speed change. +create or replace function compat_speed_guard_on_transceiver_change() returns trigger +language plpgsql as $$ +begin + if NEW.speed_gbps is distinct from OLD.speed_gbps and NEW.speed_gbps is not null then + update compatibility c + set status = 'incompatible', + notes = case when c.notes is null or c.notes = '' + then 'auto(138): transceiver speed exceeds switch max port speed (post-hoc drift)' + else c.notes || ' | auto(138): transceiver speed exceeds switch max port speed (post-hoc drift)' end + from v_switch_effective_max e + where e.switch_id = c.switch_id + and c.transceiver_id = NEW.id + and c.status = 'compatible' + and c.verification_method = 'spec_match' + and e.effective_max_gbps > 0 + and NEW.speed_gbps > e.effective_max_gbps; + end if; + return NEW; +end $$; + +comment on function compat_speed_guard_on_transceiver_change() is + 'AFTER UPDATE OF speed_gbps on transceivers: re-validates existing compatible spec_match rows for this transceiver against the evidence-based switch ceiling, closing the drift gap left by compat_speed_guard() (126), which only fires on compatibility writes. Never touches curated rows.'; + +drop trigger if exists trg_compat_speed_guard_on_transceiver_change on transceivers; +create trigger trg_compat_speed_guard_on_transceiver_change + after update of speed_gbps on transceivers + for each row execute function compat_speed_guard_on_transceiver_change(); + +insert into _migrations (filename, applied_at) +select '138-transceiver-speed-drift-guard.sql', now() +where not exists (select 1 from _migrations where filename = '138-transceiver-speed-drift-guard.sql');