diff --git a/main.go b/main.go index 7ef8dc6..ffa5522 100644 --- a/main.go +++ b/main.go @@ -87,19 +87,50 @@ func main() { // one row out of 6,755. A separate column is untouched by that, and "when // was this published" is worth knowing regardless. // - // The backfill is not optional and must land in the same deploy. Membership - // of a store catalogue is currently the *existence* of the row, so switching - // the read to `publishedat IS NOT NULL` without this empties every shop on - // the platform at once. - if err := db.DB.Exec( - `ALTER TABLE productlocations ADD COLUMN IF NOT EXISTS publishedat timestamp`).Error; err != nil { - log.Fatal("could not add productlocations.publishedat:", err) + // The backfill is not optional and must land in the same deploy as the + // column. Membership of a store catalogue was the *existence* of the row, + // so reading `publishedat IS NOT NULL` without it empties every shop on the + // platform at once. + // + // The backfill runs ONCE — only on the boot that adds the column. + // + // It used to run on every boot, and that quietly cancelled the whole point + // of the column. `WHERE publishedat IS NULL` matches a legacy row on the + // first boot and, on every boot after, matches exactly the products + // somebody is deliberately holding back: imported, not yet priced, waiting + // for review. A restart published all of them and backdated the release to + // the row's `created`, so it did not even look recent. + // + // Observed 2026-08-31: seventeen products imported at R mart at 15:55 sat + // correctly unpublished, and the next deploy published all seventeen + // stamped 15:55. With several deploys a day, the admin-catalogue tier could + // not survive an afternoon. + // + // Detecting "the column was just added" rather than tracking a migration + // version: the question is answerable from the schema itself, so it needs + // no new table and cannot drift out of step with one. + var hadPublishedAt int64 + if err := db.DB.Raw(` + SELECT COUNT(1) FROM information_schema.columns + WHERE table_name = 'productlocations' AND column_name = 'publishedat'`). + Scan(&hadPublishedAt).Error; err != nil { + log.Fatal("could not check productlocations.publishedat:", err) } - if err := db.DB.Exec(` - UPDATE productlocations - SET publishedat = COALESCE(created, NOW()) - WHERE publishedat IS NULL`).Error; err != nil { - log.Fatal("could not backfill productlocations.publishedat:", err) + if hadPublishedAt == 0 { + // First boot with the column. Every existing row predates publication + // as a concept, and membership of this table WAS publication — so + // leaving them null would empty every shop on the platform at once. + if err := db.DB.Exec( + `ALTER TABLE productlocations ADD COLUMN IF NOT EXISTS publishedat timestamp`).Error; err != nil { + log.Fatal("could not add productlocations.publishedat:", err) + } + if err := db.DB.Exec(` + UPDATE productlocations + SET publishedat = COALESCE(created, NOW()) + WHERE publishedat IS NULL`).Error; err != nil { + log.Fatal("could not backfill productlocations.publishedat:", err) + } + log.Println("productlocations.publishedat added and backfilled (one time)") } // The catalogue's own stable key for an imported product.