package store // All SQL used by the store, grouped by table in migration order. // // Conventions: // - Statements are named , e.g. insertRelease. Fragments // combined at run time are named after what they build (listOpportunities*, // filterOpportunity*). // - Named-parameter statements (":column", bound from a struct's db tags) // say so in their comment. Everything else uses "?" placeholders. // - Reads select whole rows (*, t.*, c.*) so a column added by a migration // but not mapped to a struct field fails loudly; see opportunityRow. // - Keep comments out of the SQL strings: sqlx reads a colon followed by a // name as a parameter, even inside an SQL comment. // - Every constant here is prepared against the migrated schema by // TestQueriesPrepare (queries_test.go), which also fails if a constant is // added without being listed there. // ---------- opportunities ---------- // selectOpportunityLatestReleaseDate reads the date SaveOpportunity compares // against. const selectOpportunityLatestReleaseDate = ` SELECT latest_release_date FROM opportunities WHERE uuid = ?` // selectSourceLatestReleaseDate reads the newest latest_release_date of one // source's opportunities. ORDER BY rather than MAX keeps the column's DATETIME // type, so the driver reads it as a time; it walks the latest_release_date // index. const selectSourceLatestReleaseDate = ` SELECT latest_release_date FROM opportunities WHERE source = ? ORDER BY latest_release_date DESC LIMIT 1` // upsertOpportunity inserts or updates a opportunity. Named parameters // (types.Opportunity). // // An UPDATE on conflict, not a REPLACE: REPLACE would delete the row first, // cascading to the opportunity's releases and changing its id. const upsertOpportunity = ` INSERT INTO opportunities ( uuid, uuid_type, title, status, description, buyer_name, buyer_id, main_category, procurement_method, multi_lot, sme_suitable, vcse_suitable, start_date, end_date, gross_value, has_documents, source, latest_release_date ) VALUES ( :uuid, :uuid_type, :title, :status, :description, :buyer_name, :buyer_id, :main_category, :procurement_method, :multi_lot, :sme_suitable, :vcse_suitable, :start_date, :end_date, :gross_value, :has_documents, :source, :latest_release_date ) ON CONFLICT (uuid) DO UPDATE SET uuid_type = excluded.uuid_type, title = excluded.title, status = excluded.status, description = excluded.description, buyer_name = excluded.buyer_name, buyer_id = excluded.buyer_id, main_category = excluded.main_category, procurement_method = excluded.procurement_method, multi_lot = excluded.multi_lot, sme_suitable = excluded.sme_suitable, vcse_suitable = excluded.vcse_suitable, start_date = excluded.start_date, end_date = excluded.end_date, gross_value = excluded.gross_value, has_documents = excluded.has_documents, source = excluded.source, latest_release_date = excluded.latest_release_date` // selectOpportunityByUUID reads one opportunity into a opportunityRow. const selectOpportunityByUUID = ` SELECT t.* FROM opportunities t WHERE t.uuid = ?` // ListOpportunities is assembled by listOpportunitiesQuery as: // // listOpportunitiesSelect // [WHERE filter AND filter ...] // listOpportunitiesOrderAndPage // // Each filterOpportunity* fragment takes exactly one "?" argument, except // filterOpportunityMark* fragments, which take none. A mark fragment is true // when the mark is set and is negated with NOT to match it unset; an // opportunity with no marks row has every mark unset. const ( // listOpportunitiesSelect selects every opportunity, as t. listOpportunitiesSelect = ` SELECT t.* FROM opportunities t` // listOpportunitiesOrderAndPage orders by latest release, newest first, // and takes a page: the limit and offset arguments. listOpportunitiesOrderAndPage = ` ORDER BY t.latest_release_date DESC, t.uuid LIMIT ? OFFSET ?` // filterOpportunityStatus matches a status. filterOpportunityStatus = `t.status = ?` // filterOpportunityClassificationCode matches a classification code. filterOpportunityClassificationCode = `EXISTS ( SELECT 1 FROM opportunity_classifications tc JOIN classification_codes c ON c.id = tc.classification_id WHERE tc.opportunity_uuid = t.uuid AND c.code = ? )` // filterOpportunityTitleMatch matches a full-text search of the title. The // argument must come from ftsQuery, never raw user input. filterOpportunityTitleMatch = `t.id IN ( SELECT rowid FROM opportunities_fts WHERE opportunities_fts MATCH ? )` // filterOpportunityReleasedFrom matches a latest release on or after a // time. filterOpportunityReleasedFrom = `t.latest_release_date >= ?` // filterOpportunityReleasedBefore matches a latest release before a time. filterOpportunityReleasedBefore = `t.latest_release_date < ?` // filterOpportunityMarkHidden matches the hidden mark set. filterOpportunityMarkHidden = `EXISTS ( SELECT 1 FROM opportunity_marks m WHERE m.uuid = t.uuid AND m.hidden )` // filterOpportunityMarkSkipped matches the skipped mark set. filterOpportunityMarkSkipped = `EXISTS ( SELECT 1 FROM opportunity_marks m WHERE m.uuid = t.uuid AND m.skipped )` // filterOpportunityMarkInterested matches the interested mark set. filterOpportunityMarkInterested = `EXISTS ( SELECT 1 FROM opportunity_marks m WHERE m.uuid = t.uuid AND m.interested )` // filterOpportunityMarkBidSubmitted matches the bid submitted mark set. filterOpportunityMarkBidSubmitted = `EXISTS ( SELECT 1 FROM opportunity_marks m WHERE m.uuid = t.uuid AND m.bid_submitted )` // filterOpportunityMarkWon matches the won mark set. filterOpportunityMarkWon = `EXISTS ( SELECT 1 FROM opportunity_marks m WHERE m.uuid = t.uuid AND m.won )` // filterOpportunityMarkLost matches the lost mark set. filterOpportunityMarkLost = `EXISTS ( SELECT 1 FROM opportunity_marks m WHERE m.uuid = t.uuid AND m.lost )` ) // ---------- opportunity_releases ---------- // insertRelease stores a release; 0 rows affected means it was already // stored. Named parameters (types.OCDSReleaseJSON). const insertRelease = ` INSERT INTO opportunity_releases (uuid, release_id, release_date, data) VALUES (:uuid, :release_id, :release_date, :data) ON CONFLICT DO NOTHING` // selectReleasesByOpportunity reads a opportunity's releases, oldest first. const selectReleasesByOpportunity = ` SELECT * FROM opportunity_releases WHERE uuid = ? ORDER BY release_date, release_id` // selectReleaseExists returns a single boolean. const selectReleaseExists = ` SELECT EXISTS ( SELECT 1 FROM opportunity_releases WHERE uuid = ? AND release_id = ? )` // ---------- opportunity_release_tags ---------- // insertReleaseTag stores a tag. Named parameters (types.OCDSReleaseTags). const insertReleaseTag = ` INSERT INTO opportunity_release_tags (uuid, release_id, tag) VALUES (:uuid, :release_id, :tag) ON CONFLICT DO NOTHING` // selectReleaseTagsByOpportunity reads the tags of all of a opportunity's // releases. const selectReleaseTagsByOpportunity = ` SELECT * FROM opportunity_release_tags WHERE uuid = ? ORDER BY release_id, tag` // ---------- classification_codes ---------- // upsertClassificationCodeReturningID returns the id of the row matching // every content column, inserting it first if needed. Named parameters // (types.ClassificationCode). // // The no-op DO UPDATE (rather than DO NOTHING) makes RETURNING yield the // existing row's id on conflict. const upsertClassificationCodeReturningID = ` INSERT INTO classification_codes (is_cpv, code, classification) VALUES (:is_cpv, :code, :classification) ON CONFLICT (is_cpv, code, classification) DO UPDATE SET is_cpv = excluded.is_cpv RETURNING id` // ---------- opportunity_classifications ---------- // deleteOpportunityClassifications clears a opportunity's mappings before they // are rewritten. const deleteOpportunityClassifications = ` DELETE FROM opportunity_classifications WHERE opportunity_uuid = ?` // insertOpportunityClassification maps a opportunity to a classification code. const insertOpportunityClassification = ` INSERT INTO opportunity_classifications (opportunity_uuid, classification_id) VALUES (?, ?) ON CONFLICT DO NOTHING` // selectClassificationsInUse reads every classification code that at least // one opportunity has, once each. c.* must map entirely onto // types.ClassificationCode. const selectClassificationsInUse = ` SELECT c.* FROM classification_codes c WHERE EXISTS ( SELECT 1 FROM opportunity_classifications tc WHERE tc.classification_id = c.id ) ORDER BY c.code, c.id` // selectClassificationsForOpportunities reads the classifications of several // opportunities. "IN (?)" is expanded by sqlx.In, which must be followed by // Rebind. c.* must map entirely onto types.ClassificationCode. const selectClassificationsForOpportunities = ` SELECT tc.opportunity_uuid, c.* FROM opportunity_classifications tc JOIN classification_codes c ON c.id = tc.classification_id WHERE tc.opportunity_uuid IN (?) ORDER BY tc.opportunity_uuid, c.code, c.id` // ---------- opportunity_marks ---------- // insertOpportunityMarks gives a new opportunity its marks row; an existing // row is left alone, so saving fetched data never resets marks set by the // user. Named parameters (types.OpportunityMarks). const insertOpportunityMarks = ` INSERT INTO opportunity_marks ( uuid, hidden, interested, skipped, bid_submitted, won, lost ) VALUES ( :uuid, :hidden, :interested, :skipped, :bid_submitted, :won, :lost ) ON CONFLICT (uuid) DO NOTHING` // upsertOpportunityMarks inserts or updates an opportunity's marks. Named // parameters (types.OpportunityMarks). // // An UPDATE on conflict, not a REPLACE: REPLACE would delete the row first // which is unnecessary. const upsertOpportunityMarks = ` INSERT INTO opportunity_marks ( uuid, hidden, interested, skipped, bid_submitted, won, lost ) VALUES ( :uuid, :hidden, :interested, :skipped, :bid_submitted, :won, :lost ) ON CONFLICT (uuid) DO UPDATE SET hidden = excluded.hidden, interested = excluded.interested, skipped = excluded.skipped, bid_submitted = excluded.bid_submitted, won = excluded.won, lost = excluded.lost` // selectMarksForOpportunities reads the marks of several opportunities. // "IN (?)" is expanded by sqlx.In, which must be followed by Rebind. const selectMarksForOpportunities = ` SELECT * FROM opportunity_marks WHERE uuid IN (?) ORDER BY uuid`