migrateTasksSessionInvariant rebuilds the tasks table to enforce `CHECK (status = 'backlog' OR session_id IS NOT NULL)`. SQLite does not support adding a CHECK constraint to an existing table via ALTER TABLE, so the documented procedure (CREATE new, copy, DROP old, RENAME) is used. Idempotent: probe
(db *sql.DB)
| 521 | `UPDATE tasks SET session_id=NULL, session_started=NULL, status='backlog', updated_at=? WHERE slug=?`, |
| 522 | now, l.slug, |
| 523 | ); err != nil { |
| 524 | return fmt.Errorf("demote duplicate %s: %w", l.slug, err) |
| 525 | } |
| 526 | } |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | if _, err := db.Exec( |
| 531 | `CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_session_id ON tasks(session_id) WHERE session_id IS NOT NULL`, |
| 532 | ); err != nil { |
| 533 | return fmt.Errorf("create unique index: %w", err) |
| 534 | } |
| 535 | return nil |
| 536 | } |
| 537 | |
| 538 | // migrateTasksSessionInvariant rebuilds the tasks table to enforce |
| 539 | // `CHECK (status = 'backlog' OR session_id IS NOT NULL)`. SQLite does |
| 540 | // not support adding a CHECK constraint to an existing table via |
| 541 | // ALTER TABLE, so the documented procedure (CREATE new, copy, DROP |
| 542 | // old, RENAME) is used. Idempotent: probes sqlite_master for the |
| 543 | // CHECK substring before doing any work, so subsequent calls are |
| 544 | // no-ops. Existing rows that violate the new constraint are demoted |
| 545 | // to backlog first (with a stderr summary), since there is no way to |
| 546 | // invent a session_id for them after the fact. |
| 547 | func migrateTasksSessionInvariant(db *sql.DB) error { |
| 548 | var ddl string |
| 549 | if err := db.QueryRow( |
| 550 | `SELECT sql FROM sqlite_master WHERE type='table' AND name='tasks'`, |
| 551 | ).Scan(&ddl); err != nil { |
| 552 | return fmt.Errorf("inspect tasks ddl: %w", err) |
| 553 | } |
| 554 | if strings.Contains(ddl, "session_id IS NOT NULL") { |
| 555 | return nil |
| 556 | } |
| 557 | |
| 558 | type violator struct{ slug, prevStatus string } |
| 559 | var vs []violator |
| 560 | rows, err := db.Query( |
| 561 | `SELECT slug, status FROM tasks WHERE status != 'backlog' AND session_id IS NULL`, |
| 562 | ) |
| 563 | if err != nil { |
| 564 | return fmt.Errorf("scan violators: %w", err) |
| 565 | } |
| 566 | for rows.Next() { |
| 567 | var v violator |
| 568 | if err := rows.Scan(&v.slug, &v.prevStatus); err != nil { |
| 569 | rows.Close() |
| 570 | return err |
| 571 | } |
| 572 | vs = append(vs, v) |
| 573 | } |
| 574 | if err := rows.Err(); err != nil { |
| 575 | rows.Close() |
| 576 | return err |
| 577 | } |
| 578 | rows.Close() |
| 579 | |
| 580 | if len(vs) > 0 { |
no test coverage detected