{"record":{"id":"797338b2abd8c04d","repo":"gastownhall/beads","slug":"adding-wisps-is-blocked-w","errorCode":null,"errorMessage":"adding wisps.is_blocked: %w","messagePattern":"adding wisps\\.is_blocked: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/storage/schema/migration_repairs.go","lineNumber":281,"sourceCode":"\t}\n\tif err := ensureWispIsBlockedColumn(ctx, db); err != nil {\n\t\treturn err\n\t}\n\treturn ensureWispIsBlockedIndex(ctx, db)\n}\n\n// ensureWispIsBlockedColumn is ignored/0006's ADD COLUMN statement,\n// translated to Go for a clone that reached this repair without it.\nfunc ensureWispIsBlockedColumn(ctx context.Context, db DBConn) error {\n\thasColumn, err := schemaColumnExists(ctx, db, \"wisps\", \"is_blocked\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checking wisps.is_blocked column: %w\", err)\n\t}\n\tif hasColumn {\n\t\treturn nil\n\t}\n\tif _, err := db.ExecContext(ctx, \"ALTER TABLE wisps ADD COLUMN is_blocked TINYINT(1) NOT NULL DEFAULT 0\"); err != nil {\n\t\treturn fmt.Errorf(\"adding wisps.is_blocked: %w\", err)\n\t}\n\treturn nil\n}\n\n// ensureWispIsBlockedIndex is ignored/0006's CREATE INDEX statement,\n// translated to Go alongside ensureWispIsBlockedColumn above.\nfunc ensureWispIsBlockedIndex(ctx context.Context, db DBConn) error {\n\thasIndex, err := schemaIndexExists(ctx, db, \"wisps\", \"idx_wisps_is_blocked\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checking idx_wisps_is_blocked index: %w\", err)\n\t}\n\tif hasIndex {\n\t\treturn nil\n\t}\n\tif _, err := db.ExecContext(ctx, \"CREATE INDEX idx_wisps_is_blocked ON wisps(is_blocked, status)\"); err != nil {\n\t\treturn fmt.Errorf(\"creating idx_wisps_is_blocked: %w\", err)\n\t}\n\treturn nil","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/migration_repairs.go#L263-L299","documentation":"This wraps failure of `ALTER TABLE wisps ADD COLUMN is_blocked TINYINT(1) NOT NULL DEFAULT 0`, executed when the repair confirmed the column is missing. Unlike the check errors, this is a real DDL failure — the schema was about to be changed and the statement was rejected or aborted. On MySQL this commonly fails due to permissions, a duplicate-column race, table lock contention, or insufficient disk during a table rebuild.","triggerScenarios":"ensureWispIsBlockedColumn ran on a clone whose wisps table exists without is_blocked, and the ExecContext ALTER TABLE fails: missing ALTER privilege, another process concurrently adding the same column, metadata/DDL lock timeout, or disk-full during the implicit table copy.","commonSituations":"Two instances of the tool repairing the same database simultaneously; running repairs as a read-mostly user; large wisps tables making the DDL slow and prone to lock-wait timeouts on MySQL 5.x without online DDL.","solutions":["Read the wrapped driver error: ER_DUP_FIELDNAME means another process already added the column — re-run the repair and it will no-op","Grant ALTER privilege to the migration user","Ensure only one repair runs at a time (lock or serialize repair runs)","For lock-wait timeouts, run the DDL during a quiet window or upgrade to a MySQL/Dolt version with online DDL","Free disk space if the error indicates the table rebuild ran out of room"],"exampleFix":"// before: two workers racing the same repair\ngo runRepair(db); go runRepair(db)\n// after: serialize repairs\nmu.Lock(); defer mu.Unlock()\nif err := runRepair(db); err != nil { return err }","handlingStrategy":"try-catch","validationCode":"// ensure no concurrent repair and sufficient privileges before ALTER\nvar privOK int\n_ = db.QueryRowContext(ctx,\n    \"SELECT COUNT(*) FROM information_schema.user_privileges WHERE grantee = CURRENT_USER() AND privilege_type IN ('ALTER','CREATE')\").Scan(&privOK)\nif privOK == 0 {\n    return errors.New(\"repair user lacks ALTER privilege; aborting before DDL\")\n}","typeGuard":null,"tryCatchPattern":"if err := ensureWispIsBlockedForRecompute(ctx, db); err != nil {\n    var drv *mysql.MySQLError\n    if errors.As(err, &drv) {\n        switch drv.Number {\n        case 1060: // ER_DUP_FIELDNAME — column added concurrently, safe to re-run\n            return ensureWispIsBlockedForRecompute(ctx, db)\n        case 1205: // lock wait timeout\n            return fmt.Errorf(\"DDL blocked; retry in a quiet window: %w\", err)\n        }\n    }\n    return err\n}","preventionTips":["Serialize repair runs with a file/DB lock so only one process does DDL","Grant ALTER to the migration user, not just SELECT/INSERT","Handle ER_DUP_FIELDNAME as 'already done' and re-run to confirm no-op","Run large-table DDL in a maintenance window with lock_wait_timeout raised"],"tags":["database","ddl","mysql","alter-table","schema-migration"],"backgroundTag":"alter-table-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}