{"record":{"id":"244f225ac79ce9d1","repo":"gastownhall/beads","slug":"db-childcountersqlrepository-nextchildid-upsert","errorCode":null,"errorMessage":"db: ChildCounterSQLRepository.NextChildID: upsert counter for %s: %w","messagePattern":"db: ChildCounterSQLRepository\\.NextChildID: upsert counter for (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/child_counter.go","lineNumber":81,"sourceCode":"\t\tvar id string\n\t\tif err := rows.Scan(&id); err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"db: ChildCounterSQLRepository.NextChildID: scan: %w\", err)\n\t\t}\n\t\tif n, ok := parseChildSuffix(id); ok && n > lastChild {\n\t\t\tlastChild = n\n\t\t}\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"db: ChildCounterSQLRepository.NextChildID: rows: %w\", err)\n\t}\n\n\tnext := lastChild + 1\n\t//nolint:gosec // G201: counterTable is one of two hardcoded constants\n\tif _, err := r.runner.ExecContext(ctx, fmt.Sprintf(`\n\t\tINSERT INTO %s (parent_id, last_child) VALUES (?, ?)\n\t\tON DUPLICATE KEY UPDATE last_child = ?\n\t`, counterTable), parentID, next, next); err != nil {\n\t\treturn \"\", fmt.Errorf(\"db: ChildCounterSQLRepository.NextChildID: upsert counter for %s: %w\", parentID, err)\n\t}\n\n\treturn fmt.Sprintf(\"%s.%d\", parentID, next), nil\n}\n\nfunc (r *childCounterSQLRepositoryImpl) parentIsActiveWisp(ctx context.Context, parentID string) (bool, error) {\n\tvar probe int\n\terr := r.runner.QueryRowContext(ctx, \"SELECT 1 FROM wisps WHERE id = ? LIMIT 1\", parentID).Scan(&probe)\n\tswitch {\n\tcase err == nil:\n\t\treturn true, nil\n\tcase errors.Is(err, sql.ErrNoRows):\n\t\treturn false, nil\n\tcase dberrors.IsTableNotExist(err):\n\t\treturn false, nil\n\tdefault:\n\t\treturn false, err\n\t}","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/child_counter.go#L63-L99","documentation":"NextChildID persists the computed next child number via an INSERT ... ON DUPLICATE KEY UPDATE into `child_counters` (or `wisp_child_counters`). This error wraps any ExecContext failure from that upsert. It means the child ID was computed but could not be recorded, so callers must not use the returned child ID.","triggerScenarios":"Calling NextChildID when the counter table is missing (schema not migrated), the write is rejected (read-only replica, disk full, lock wait timeout), or the context is canceled during the write.","commonSituations":"Running against a database missing the child_counters/wisp_child_counters migration; duplicate-key/lock contention under concurrent child creation; read-only connection used for a write.","solutions":["Run schema migrations to ensure `child_counters` and `wisp_child_counters` tables exist (check dberrors.IsTableNotExist on the wrapped error).","Retry under contention; serialize concurrent child-creation for the same parent (lock or retry with backoff).","Verify the connection is writable (not a read-only replica) and the server has disk space.","Check the wrapped error for lock-wait-timeout and tune innodb_lock_wait_timeout or reduce transaction scope."],"exampleFix":"// before: counter table missing after manual DB setup\n// ERROR: Table 'beads.child_counters' doesn't exist\n\n// after: ensure migrations ran before repository use\nif err := db.Migrate(conn); err != nil { log.Fatal(err) }","handlingStrategy":"validation","validationCode":"var exists int\nif err := conn.QueryRowContext(ctx,\n    \"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'child_counters'\").Scan(&exists); err != nil || exists == 0 {\n    return fmt.Errorf(\"child_counters table missing; run migrations\")\n}","typeGuard":"func isUpsertError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"NextChildID: upsert counter\")\n}","tryCatchPattern":"id, err := repo.NextChildID(ctx, parentID, opts)\nif err != nil && isUpsertError(err) {\n    if dberrors.IsTableNotExist(err) { migrate(); return repo.NextChildID(ctx, parentID, opts) }\n    return err // lock contention / read-only: surface to caller\n}","preventionTips":["Run schema migrations at startup","Serialize concurrent child creation per parent","Confirm the connection isn't read-only before writes"],"tags":["database","sql","upsert","go"],"backgroundTag":"sql-write-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}