diff --git a/sql/141-switch-speed-drift-guard.sql b/sql/141-switch-speed-drift-guard.sql new file mode 100644 index 0000000..17afa08 --- /dev/null +++ b/sql/141-switch-speed-drift-guard.sql @@ -0,0 +1,70 @@ +-- 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');