-- Opportunity Intelligence unified records. Global public catalog (no organization_id, outside RLS): -- reads run straight off the pool, writes come from cmd/opp-sync as the migrator role. -- name: UpsertOpportunityRecord :exec INSERT INTO opportunity_records ( source_name, source_record_id, dedupe_key, record_kind, title, agency, sub_agency, naics, psc, pop_city, pop_state, pop_country, pop_zip, set_aside, set_aside_code, contract_vehicle, estimated_value, value_source, incumbent_name, incumbent_uei, posted_date, response_deadline, award_date, estimated_award_date, pop_start_date, pop_end_date, ui_link, is_planning, raw_object_key, raw_sha256, fetched_at, synced_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, now() ) ON CONFLICT (source_name, source_record_id) DO UPDATE SET dedupe_key = EXCLUDED.dedupe_key, record_kind = EXCLUDED.record_kind, title = EXCLUDED.title, agency = EXCLUDED.agency, sub_agency = EXCLUDED.sub_agency, naics = EXCLUDED.naics, psc = EXCLUDED.psc, pop_city = EXCLUDED.pop_city, pop_state = EXCLUDED.pop_state, pop_country = EXCLUDED.pop_country, pop_zip = EXCLUDED.pop_zip, set_aside = EXCLUDED.set_aside, set_aside_code = EXCLUDED.set_aside_code, contract_vehicle = EXCLUDED.contract_vehicle, estimated_value = EXCLUDED.estimated_value, value_source = EXCLUDED.value_source, incumbent_name = EXCLUDED.incumbent_name, incumbent_uei = EXCLUDED.incumbent_uei, posted_date = EXCLUDED.posted_date, response_deadline = EXCLUDED.response_deadline, award_date = EXCLUDED.award_date, estimated_award_date = EXCLUDED.estimated_award_date, pop_start_date = EXCLUDED.pop_start_date, pop_end_date = EXCLUDED.pop_end_date, ui_link = EXCLUDED.ui_link, is_planning = EXCLUDED.is_planning, raw_object_key = EXCLUDED.raw_object_key, raw_sha256 = EXCLUDED.raw_sha256, fetched_at = EXCLUDED.fetched_at, synced_at = now(); -- name: GetOpportunityRecord :one SELECT * FROM opportunity_records WHERE id = $1; -- name: OpportunityRecordMeta :one SELECT COUNT(*)::bigint AS total, MAX(synced_at) AS last_synced FROM opportunity_records; -- name: ListOpportunityRecords :many -- Optional filters (NULL array = no filter). Soonest closing first, then most recently posted, then id -- as a unique tiebreaker so the order is deterministic. Text equality filters compare upper() on both -- sides; the service upper cases the params so casing drift cannot silently zero out results. The -- optional missions filter keeps a record only when a federal_mission_tag links it to one of the named -- mission codes; a NULL missions array means no mission filter, so the default path is unchanged. SELECT * FROM opportunity_records WHERE (sqlc.narg('kinds')::text[] IS NULL OR record_kind = ANY(sqlc.narg('kinds')::text[])) AND (sqlc.narg('naics')::text[] IS NULL OR naics = ANY(sqlc.narg('naics')::text[])) AND (sqlc.narg('agencies')::text[] IS NULL OR upper(agency) = ANY(sqlc.narg('agencies')::text[])) AND (sqlc.narg('states')::text[] IS NULL OR upper(pop_state) = ANY(sqlc.narg('states')::text[])) AND (sqlc.narg('missions')::text[] IS NULL OR EXISTS ( SELECT 1 FROM federal_mission_tags t WHERE t.record_table = 'opportunity_records' AND t.record_id = opportunity_records.id::text AND t.mission_code = ANY(sqlc.narg('missions')::text[]))) ORDER BY (response_deadline IS NULL OR response_deadline < now()), response_deadline ASC, posted_date DESC NULLS LAST, id LIMIT sqlc.arg('lim')::int;