| index 070bd34e9..34199aec2 100644 |
| --- a/internal/console/billingquantityaudit.go |
| +++ b/internal/console/billingquantityaudit.go |
| @@ -1,6 +1,9 @@ |
| package console |
| |
| -import "database/sql" |
| +import ( |
| + "database/sql" |
| + "errors" |
| +) |
| |
| func (s *Store) migrateQuantityHistoryGuards() error { |
| for _, col := range []string{"processing_outcome", "processing_reference"} { |
| @@ -8,29 +11,38 @@ func (s *Store) migrateQuantityHistoryGuards() error { |
| return err |
| } |
| } |
| - for _, statement := range []string{ |
| - `CREATE TRIGGER IF NOT EXISTS billing_quantity_events_insert_guard BEFORE INSERT ON billing_quantity_events |
| + // The processing guard's body names the outcomes it admits, so a |
| + // database created before an outcome was added keeps a guard that |
| + // refuses it. The guard is replaced when its stored text differs from |
| + // this one, and only then, in one immediate transaction: two console |
| + // processes opening the same database cannot meet between the drop and |
| + // the create, and a database that is already current is not touched. |
| + const processingGuard = `CREATE TRIGGER billing_quantity_events_processing_guard BEFORE UPDATE ON billing_quantity_events |
| + WHEN NEW.processed_at<=0 OR NEW.processed_at<OLD.occurred_at_ns/1000000000 |
| + OR NEW.processing_outcome NOT IN ('charged','included','landed_unverified','checkout_expired','checkout_abandoned') |
| + OR length(trim(NEW.processing_reference))=0 OR length(NEW.processing_reference)>256 |
| + BEGIN SELECT RAISE(ABORT,'billing_quantity_processing_invalid'); END` |
| + if _, err := s.db.Exec(`CREATE TRIGGER IF NOT EXISTS billing_quantity_events_insert_guard BEFORE INSERT ON billing_quantity_events |
| WHEN NEW.processed_at<>0 OR NEW.processing_outcome<>'' OR NEW.processing_reference<>'' |
| OR NOT ((NEW.change='addition' AND NEW.effect='prorate' AND NEW.members_after=NEW.members_before+1) |
| OR (NEW.change='removal' AND NEW.effect='at_renewal' AND NEW.members_after=NEW.members_before-1)) |
| OR EXISTS(SELECT 1 FROM billing_quantity_events WHERE seq=NEW.seq OR (account_id=NEW.account_id AND generation=NEW.generation)) |
| - BEGIN SELECT RAISE(ABORT,'billing_quantity_events_append_only'); END`, |
| - // Dropped and recreated, not IF NOT EXISTS: the set of outcomes the |
| - // guard admits is part of its body, and a database created before |
| - // an outcome was added would otherwise keep refusing it. |
| - `DROP TRIGGER IF EXISTS billing_quantity_events_processing_guard`, |
| - `CREATE TRIGGER billing_quantity_events_processing_guard BEFORE UPDATE ON billing_quantity_events |
| - WHEN NEW.processed_at<=0 OR NEW.processed_at<OLD.occurred_at_ns/1000000000 |
| - OR NEW.processing_outcome NOT IN ('charged','included','landed_unverified','checkout_expired','checkout_abandoned') |
| - OR length(trim(NEW.processing_reference))=0 OR length(NEW.processing_reference)>256 |
| - BEGIN SELECT RAISE(ABORT,'billing_quantity_processing_invalid'); END`, |
| - `UPDATE billing_quantity_changes SET history_gap=1,state='history_unproven' |
| - WHERE EXISTS(SELECT 1 FROM billing_quantity_events e WHERE e.account_id=billing_quantity_changes.account_id AND e.processed_at<>0 AND (e.processed_at<e.occurred_at_ns/1000000000 OR e.processing_outcome NOT IN ('charged','included','landed_unverified','checkout_expired','checkout_abandoned') OR length(trim(e.processing_reference))=0 OR length(e.processing_reference)>256))`, |
| - } { |
| - if _, err := s.db.Exec(statement); err != nil { |
| + BEGIN SELECT RAISE(ABORT,'billing_quantity_events_append_only'); END`); err != nil { |
| + return err |
| + } |
| + var stored sql.NullString |
| + if err := s.db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='trigger' AND name='billing_quantity_events_processing_guard'`).Scan(&stored); err != nil && !errors.Is(err, sql.ErrNoRows) { |
| + return err |
| + } |
| + if !stored.Valid || stored.String != processingGuard { |
| + if _, err := s.db.Exec(`BEGIN IMMEDIATE; DROP TRIGGER IF EXISTS billing_quantity_events_processing_guard; ` + processingGuard + `; COMMIT`); err != nil { |
| return err |
| } |
| } |
| + if _, err := s.db.Exec(`UPDATE billing_quantity_changes SET history_gap=1,state='history_unproven' |
| + WHERE EXISTS(SELECT 1 FROM billing_quantity_events e WHERE e.account_id=billing_quantity_changes.account_id AND e.processed_at<>0 AND (e.processed_at<e.occurred_at_ns/1000000000 OR e.processing_outcome NOT IN ('charged','included','landed_unverified','checkout_expired','checkout_abandoned') OR length(trim(e.processing_reference))=0 OR length(e.processing_reference)>256))`); err != nil { |
| + return err |
| + } |
| return nil |
| } |
| |