diff --git a/packages/api/src/db/queries.ts b/packages/api/src/db/queries.ts index d5259e8..22777d0 100644 --- a/packages/api/src/db/queries.ts +++ b/packages/api/src/db/queries.ts @@ -318,23 +318,40 @@ export async function getCompatibleTransceivers(switchId: string) { */ export async function getFlexoptixSuggestions(switchId: string) { const result = await pool.query( - `WITH switch_form_factors AS ( + `WITH switch_ports AS ( + -- Parse each ports_config key 'SPEED_FORMFACTOR' (e.g. '100G_QSFP28') into + -- a numeric port speed (Gbps) + a cage family. Real transceivers are then + -- bounded by the port speed so corrupt/oversized records cannot leak in. SELECT DISTINCT + -- numeric speed prefix: 100G->100, 25G->25, 2.5G->2.5, 100M->0.1, 800G->800 + CASE + WHEN k ~ '^[0-9.]+M' THEN (substring(k from '^([0-9.]+)M')::numeric / 1000) + WHEN k ~ '^[0-9.]+G' THEN substring(k from '^([0-9.]+)G')::numeric + WHEN k ~ '^[0-9.]+T' THEN (substring(k from '^([0-9.]+)T')::numeric * 1000) + ELSE NULL + END AS port_speed, + -- the port's specific cage type (determines which modules mechanically seat) CASE WHEN k ILIKE '%QSFP-DD800%' THEN 'QSFP-DD800' WHEN k ILIKE '%QSFP-DD%' THEN 'QSFP-DD' - WHEN k ILIKE '%OSFP224%' THEN 'OSFP224' - WHEN k ILIKE '%OSFP%' THEN 'OSFP' + WHEN k ILIKE '%QSFP112%' THEN 'QSFP112' + WHEN k ILIKE '%QSFP56%' THEN 'QSFP56' WHEN k ILIKE '%QSFP28%' THEN 'QSFP28' WHEN k ILIKE '%QSFP+%' THEN 'QSFP+' WHEN k ILIKE '%QSFP%' THEN 'QSFP+' + WHEN k ILIKE '%OSFP224%' THEN 'OSFP224' + WHEN k ILIKE '%OSFP112%' THEN 'OSFP112' + WHEN k ILIKE '%OSFP%' THEN 'OSFP' + WHEN k ILIKE '%SFP56%' THEN 'SFP56' WHEN k ILIKE '%SFP28%' THEN 'SFP28' WHEN k ILIKE '%SFP+%' THEN 'SFP+' WHEN k ILIKE '%SFP%' THEN 'SFP+' WHEN k ILIKE '%CFP2%' THEN 'CFP2' WHEN k ILIKE '%CFP4%' THEN 'CFP4' WHEN k ILIKE '%CFP%' THEN 'CFP' - END AS form_factor + WHEN k ILIKE '%RJ45%' OR k ILIKE '%mGig%' THEN 'RJ45' + ELSE NULL + END AS port_ff FROM switches sw, jsonb_object_keys(sw.ports_config) AS k WHERE sw.id = $1 AND sw.ports_config IS NOT NULL @@ -367,8 +384,38 @@ export async function getFlexoptixSuggestions(switchId: string) { LIMIT 1 ) so ON true WHERE LOWER(v.name) = 'flexoptix' - AND t.form_factor IN ( - SELECT form_factor FROM switch_form_factors WHERE form_factor IS NOT NULL + AND t.speed_gbps IS NOT NULL AND t.speed_gbps > 0 + AND EXISTS ( + SELECT 1 FROM switch_ports sp + WHERE sp.port_ff IS NOT NULL + AND sp.port_speed IS NOT NULL + -- module must mechanically seat in this port cage. A cage accepts its own + -- type plus smaller/backward-compatible modules, never a larger one: + -- QSFP-DD/OSFP cages take QSFP-family; QSFP28/56 cages take QSFP+/28/56 + -- (NOT QSFP-DD); SFP cages take all SFP variants. No cross-family. + AND t.form_factor = ANY ( + CASE sp.port_ff + WHEN 'QSFP-DD800' THEN ARRAY['QSFP-DD800','QSFP-DD','QSFP112','QSFP56','QSFP28','QSFP+'] + WHEN 'QSFP-DD' THEN ARRAY['QSFP-DD','QSFP112','QSFP56','QSFP28','QSFP+'] + WHEN 'QSFP112' THEN ARRAY['QSFP112','QSFP56','QSFP28','QSFP+'] + WHEN 'QSFP56' THEN ARRAY['QSFP56','QSFP28','QSFP+'] + WHEN 'QSFP28' THEN ARRAY['QSFP28','QSFP+'] + WHEN 'QSFP+' THEN ARRAY['QSFP+'] + WHEN 'OSFP224' THEN ARRAY['OSFP224','OSFP112','OSFP'] + WHEN 'OSFP112' THEN ARRAY['OSFP112','OSFP'] + WHEN 'OSFP' THEN ARRAY['OSFP'] + WHEN 'SFP56' THEN ARRAY['SFP56','SFP28','SFP+','SFP'] + WHEN 'SFP28' THEN ARRAY['SFP28','SFP+','SFP'] + WHEN 'SFP+' THEN ARRAY['SFP+','SFP'] + WHEN 'CFP2' THEN ARRAY['CFP2'] + WHEN 'CFP4' THEN ARRAY['CFP4'] + WHEN 'CFP' THEN ARRAY['CFP'] + WHEN 'RJ45' THEN ARRAY['RJ45','Copper'] + ELSE ARRAY[]::text[] + END + ) + -- speed must not exceed the port's speed (slower module in faster cage OK) + AND t.speed_gbps <= sp.port_speed ) ORDER BY t.speed_gbps DESC NULLS LAST, t.reach_meters ASC NULLS LAST`, [switchId]