-- Evolves scenario_attempts (Phase 2B) from "one row per validation click" into a real attempt -- lifecycle: one row per continuous working session on a scenario, with aggregated telemetry -- counters, updated in place as the student works — not a new table (Phase 3B explicitly asks -- to extend/refine rather than duplicate). See platform/api/src/attempts.js. -- -- Backward compatible: every new column is nullable or has a DEFAULT, so Phase 2B's existing -- rows (real historical data from testing) stay valid without being dropped or rewritten in -- shape — the backfill UPDATE below just gives them sane values under the new semantics. -- Phase 2B required `result` on every row (it was always written at completion time, atomically). -- Under the new attempt lifecycle a row can exist mid-attempt (status='in_progress') with no -- result yet — result is only ever set once the attempt reaches a terminal state. ALTER TABLE scenario_attempts ALTER COLUMN result DROP NOT NULL; -- hint_count (migration 005) was deliberately left NULL / undefaulted — Phase 2B's note was -- "not measurable today." It's measurable now (this is the phase that builds real hint -- tracking), so it needs a DEFAULT 0 for new rows to actually increment from, and existing NULL -- rows backfilled to 0 (same "not measured" meaning either way, just now a real zero). ALTER TABLE scenario_attempts ALTER COLUMN hint_count SET DEFAULT 0; UPDATE scenario_attempts SET hint_count = 0 WHERE hint_count IS NULL; ALTER TABLE scenario_attempts ADD COLUMN IF NOT EXISTS cohort_id TEXT; ALTER TABLE scenario_attempts ADD COLUMN IF NOT EXISTS curriculum_stage TEXT; ALTER TABLE scenario_attempts ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'completed'; ALTER TABLE scenario_attempts ADD COLUMN IF NOT EXISTS validation_attempt_count INTEGER NOT NULL DEFAULT 0; ALTER TABLE scenario_attempts ADD COLUMN IF NOT EXISTS validation_pass_count INTEGER NOT NULL DEFAULT 0; ALTER TABLE scenario_attempts ADD COLUMN IF NOT EXISTS validation_fail_count INTEGER NOT NULL DEFAULT 0; ALTER TABLE scenario_attempts ADD COLUMN IF NOT EXISTS highest_hint_level INTEGER NOT NULL DEFAULT 0; ALTER TABLE scenario_attempts ADD COLUMN IF NOT EXISTS duration_seconds INTEGER; ALTER TABLE scenario_attempts ADD COLUMN IF NOT EXISTS assessment_session_id UUID; DO $$ BEGIN ALTER TABLE scenario_attempts ADD CONSTRAINT scenario_attempts_status_check CHECK (status IN ('in_progress', 'completed', 'abandoned')); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- Backfill: Phase 2B rows were written atomically at completion time (one INSERT per validate -- call, already carrying completed_at + result) — under the new semantics that's a completed -- attempt with exactly one validation performed. UPDATE scenario_attempts SET status = 'completed', validation_attempt_count = 1, validation_pass_count = CASE WHEN result = 'passed' THEN 1 ELSE 0 END, validation_fail_count = CASE WHEN result = 'failed' THEN 1 ELSE 0 END WHERE validation_attempt_count = 0 AND completed_at IS NOT NULL; -- "Find the open attempt for this student+scenario+environment" is the hot lookup path (every -- hint reveal and validate call needs it) — environment-scoped so a Lab recreate doesn't -- silently resume an attempt tied to a destroyed environment. CREATE INDEX IF NOT EXISTS idx_attempts_open_lookup ON scenario_attempts(student_id, scenario_id, environment_id, status); CREATE INDEX IF NOT EXISTS idx_attempts_cohort ON scenario_attempts(cohort_id); CREATE INDEX IF NOT EXISTS idx_attempts_assessment_session ON scenario_attempts(assessment_session_id);