-- sql/141-switch-speed-drift-guard.sql -- Preventive close of the THIRD direction of the same drift-gap class fixed in -- sql/126 (compat_speed_guard, guards compatibility writes) and sql/138 -- (compat_speed_guard_on_transceiver_change, guards transceivers.speed_gbps -- changes). The "no physically-impossible compatibility rows" invariant is -- derived from THREE tables' data: compatibility.status, transceivers.speed_gbps, -- and switches.max_speed_gbps/ports_config (via v_switch_effective_max). Only -- two of those three write paths were guarded. -- -- Audit 2026-07-17 found switches.max_speed_gbps/ports_config are currently -- write-once (set at scrape/seed time, sql/125's one-time backfill) — no live -- job updates them today, so this direction is NOT actively exploited yet. But -- the documented open item "110 switches with understated ports_config need -- datasheet enrichment" ([[tip-switch-database-audit-2026-07-06]]) describes -- exactly the future job that WOULD trigger this gap: correcting a switch's -- max_speed_gbps or ports_config downward after compat rows already exist would -- silently leave now-impossible rows marked 'compatible', same failure mode as -- sql/138, just from the other table. Closing it now, before that automation -- ships, rather than waiting for the next backlog surprise. -- -- Same "no false positives" design as 126/138: only algorithmic (non-curated) -- rows are ever forced to incompatible; curated rows are trusted as-is. create or replace function compat_speed_guard_on_switch_change() returns trigger language plpgsql as $$ declare new_ceiling numeric; begin if NEW.max_speed_gbps is distinct from OLD.max_speed_gbps or NEW.ports_config is distinct from OLD.ports_config then select greatest( coalesce(NEW.max_speed_gbps, 0), coalesce(fn_ports_max_gbps(NEW.ports_config), 0), coalesce((select max(t.speed_gbps) from compatibility c join transceivers t on t.id = c.transceiver_id where c.switch_id = NEW.id and c.status = 'compatible' and c.verification_method in ('vendor_matrix','vendor_compat') and t.speed_gbps > 0), 0) ) into new_ceiling; if new_ceiling > 0 then update compatibility c set status = 'incompatible', notes = case when c.notes is null or c.notes = '' then 'auto(141): transceiver speed exceeds switch max port speed (post-hoc drift)' else c.notes || ' | auto(141): transceiver speed exceeds switch max port speed (post-hoc drift)' end from transceivers t where t.id = c.transceiver_id and c.switch_id = NEW.id and c.status = 'compatible' and c.verification_method = 'spec_match' and t.speed_gbps is not null and t.speed_gbps > new_ceiling; end if; end if; return NEW; end $$; comment on function compat_speed_guard_on_switch_change() is 'AFTER UPDATE OF max_speed_gbps/ports_config on switches: re-validates existing compatible spec_match compatibility rows for this switch against the (re-derived) evidence-based ceiling. Third leg of the compat_speed_guard family (126=compatibility writes, 138=transceiver speed changes, this=switch capacity changes). Never touches curated rows. Currently a no-op in practice -- nothing updates these columns live yet -- this is preventive, closing the gap before the planned switch datasheet-enrichment automation ships.'; drop trigger if exists trg_compat_speed_guard_on_switch_change on switches; create trigger trg_compat_speed_guard_on_switch_change after update of max_speed_gbps, ports_config on switches for each row execute function compat_speed_guard_on_switch_change(); insert into _migrations (filename, applied_at) select '141-switch-speed-drift-guard.sql', now() where not exists (select 1 from _migrations where filename = '141-switch-speed-drift-guard.sql');