(uid int64, f ProgramFilter)
| 25 | return row.Scan(&p.ID, &p.UserID, &p.Name, &p.Notes, &p.CreatedAt) |
| 26 | } |
| 27 | |
| 28 | func (s *ProgramStore) List(uid int64, f ProgramFilter) ([]models.Program, error) { |
| 29 | var rows *sql.Rows |
| 30 | var err error |
| 31 | if f.Query != "" { |
| 32 | rows, err = s.db.Query( |
| 33 | `SELECT `+programCols+` FROM programs WHERE user_id = ? AND LOWER(name) LIKE ? ORDER BY created_at DESC LIMIT ? OFFSET ?`, |
| 34 | uid, "%"+strings.ToLower(f.Query)+"%", f.Limit, f.Offset, |
| 35 | ) |
| 36 | } else { |
| 37 | rows, err = s.db.Query( |
| 38 | `SELECT `+programCols+` FROM programs WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`, |
| 39 | uid, f.Limit, f.Offset, |
| 40 | ) |
| 41 | } |
| 42 | if err != nil { |
| 43 | return nil, err |
| 44 | } |
| 45 | programs := []models.Program{} |
| 46 | for rows.Next() { |
| 47 | var p models.Program |
| 48 | if err := scanProgram(rows, &p); err != nil { |
| 49 | rows.Close() |
| 50 | return nil, err |
| 51 | } |
| 52 | programs = append(programs, p) |
| 53 | } |
| 54 | if err := rows.Err(); err != nil { |
| 55 | rows.Close() |
| 56 | return nil, err |
| 57 | } |
| 58 | rows.Close() // close the parent cursor BEFORE loading children (#36) |
| 59 | |
| 60 | // loadDays (exercises/sets) stays a per-program call — pre-existing N+1, out of |
| 61 | // scope here. currentDayIndex is batched below instead of called per-program: |
| 62 | // unlike loadDays it added 2 brand-new sequential round trips per program on top |
| 63 | // of that, and SetMaxOpenConns(1) serializes every one of them on the process's |
| 64 | // single connection. |
| 65 | for i := range programs { |
| 66 | days, err := s.loadDays(programs[i].ID) |
| 67 | if err != nil { |
| 68 | return nil, err |
| 69 | } |
| 70 | programs[i].Days = days |
nothing calls this directly
no test coverage detected