Function bodies 254 total
DeleteIssue method · go · L147-L155 (9 LOC)backend/internal/service/service.go
func (s *Service) DeleteIssue(ctx context.Context, id string) error {
iss, _ := s.repo.Issues().GetByID(ctx, id)
err := s.repo.Issues().Delete(ctx, id)
if err != nil {
return err
}
s.DispatchWebhook(iss.BoardID, domain.EventIssueDeleted, &iss)
return nil
}GetIssue method · go · L157-L164 (8 LOC)backend/internal/service/service.go
func (s *Service) GetIssue(ctx context.Context, id string) (issue domain.Issue, children []domain.Issue, err error) {
issue, err = s.repo.Issues().GetByID(ctx, id)
if err != nil {
return
}
children, err = s.repo.Issues().ListChildren(ctx, id)
return
}ListIssues method · go · L166-L168 (3 LOC)backend/internal/service/service.go
func (s *Service) ListIssues(ctx context.Context, f storage.IssueFilter) ([]domain.Issue, error) {
return s.repo.Issues().List(ctx, f)
}ReorderIssues method · go · L170-L172 (3 LOC)backend/internal/service/service.go
func (s *Service) ReorderIssues(ctx context.Context, issueIDs []string) error {
return s.repo.Issues().ReorderIssues(ctx, issueIDs)
}AddDependency method · go · L174-L176 (3 LOC)backend/internal/service/service.go
func (s *Service) AddDependency(ctx context.Context, blockerID, blockedID string) error {
return s.repo.Issues().AddDependency(ctx, blockerID, blockedID)
}RemoveDependency method · go · L178-L180 (3 LOC)backend/internal/service/service.go
func (s *Service) RemoveDependency(ctx context.Context, blockerID, blockedID string) error {
return s.repo.Issues().RemoveDependency(ctx, blockerID, blockedID)
}GetBlockedBy method · go · L182-L184 (3 LOC)backend/internal/service/service.go
func (s *Service) GetBlockedBy(ctx context.Context, issueID string) ([]string, error) {
return s.repo.Issues().GetBlockedBy(ctx, issueID)
}Same scanner, your repo: https://repobility.com — Repobility
CreateComment method · go · L188-L190 (3 LOC)backend/internal/service/service.go
func (s *Service) CreateComment(ctx context.Context, issueID, body string) (*domain.Comment, error) {
return s.repo.Comments().Create(ctx, issueID, body)
}ListComments method · go · L192-L194 (3 LOC)backend/internal/service/service.go
func (s *Service) ListComments(ctx context.Context, issueID string) ([]domain.Comment, error) {
return s.repo.Comments().List(ctx, issueID)
}DeleteComment method · go · L196-L198 (3 LOC)backend/internal/service/service.go
func (s *Service) DeleteComment(ctx context.Context, commentID string) error {
return s.repo.Comments().Delete(ctx, commentID)
}CreateBoard method · go · L202-L219 (18 LOC)backend/internal/service/service.go
func (s *Service) CreateBoard(ctx context.Context, name string, viewType domain.ViewType) (domain.Board, error) {
if name == "" {
return domain.Board{}, errors.New("board name must not be empty")
}
if viewType == "" {
viewType = domain.ViewKanban
}
if !viewType.Valid() {
return domain.Board{}, errors.New("invalid viewType")
}
b := domain.Board{
ID: uuid.NewString(),
Name: name,
ViewType: viewType,
CreatedAt: s.now(),
}
return s.repo.Boards().Create(ctx, b)
}ListBoards method · go · L221-L223 (3 LOC)backend/internal/service/service.go
func (s *Service) ListBoards(ctx context.Context) ([]domain.Board, error) {
return s.repo.Boards().List(ctx)
}GetBoard method · go · L225-L227 (3 LOC)backend/internal/service/service.go
func (s *Service) GetBoard(ctx context.Context, id string) (domain.Board, error) {
return s.repo.Boards().GetByID(ctx, id)
}UpdateBoard method · go · L229-L234 (6 LOC)backend/internal/service/service.go
func (s *Service) UpdateBoard(ctx context.Context, id string, name *string, viewType *domain.ViewType) (domain.Board, error) {
if viewType != nil && !viewType.Valid() {
return domain.Board{}, errors.New("invalid viewType")
}
return s.repo.Boards().Update(ctx, id, name, viewType)
}DeleteBoard method · go · L236-L238 (3 LOC)backend/internal/service/service.go
func (s *Service) DeleteBoard(ctx context.Context, id string) error {
return s.repo.Boards().Delete(ctx, id)
}Powered by Repobility — scan your code at https://repobility.com
UpdateIssueProperties method · go · L243-L260 (18 LOC)backend/internal/service/service.go
func (s *Service) UpdateIssueProperties(ctx context.Context, id string, props map[string]any) (domain.Issue, error) {
cur, err := s.repo.Issues().GetByID(ctx, id)
if err != nil {
return domain.Issue{}, err
}
if cur.Properties == nil {
cur.Properties = map[string]any{}
}
for k, v := range props {
if v == nil {
delete(cur.Properties, k)
} else {
cur.Properties[k] = v
}
}
merged := cur.Properties
return s.repo.Issues().Update(ctx, id, storage.IssuePatch{Properties: &merged})
}CreateBoardProperty method · go · L264-L283 (20 LOC)backend/internal/service/service.go
func (s *Service) CreateBoardProperty(ctx context.Context, boardID, name string, propType domain.PropertyType, options []string) (domain.BoardProperty, error) {
if name == "" {
return domain.BoardProperty{}, errors.New("property name must not be empty")
}
if !propType.Valid() {
return domain.BoardProperty{}, errors.New("invalid property type")
}
if options == nil {
options = []string{}
}
existing, _ := s.repo.BoardProperties().List(ctx, boardID)
p := domain.BoardProperty{
BoardID: boardID,
Name: name,
Type: propType,
Options: options,
Position: len(existing),
}
return s.repo.BoardProperties().Create(ctx, p)
}ListBoardProperties method · go · L285-L287 (3 LOC)backend/internal/service/service.go
func (s *Service) ListBoardProperties(ctx context.Context, boardID string) ([]domain.BoardProperty, error) {
return s.repo.BoardProperties().List(ctx, boardID)
}UpdateBoardProperty method · go · L289-L291 (3 LOC)backend/internal/service/service.go
func (s *Service) UpdateBoardProperty(ctx context.Context, id string, name *string, options *[]string, position *int) (domain.BoardProperty, error) {
return s.repo.BoardProperties().Update(ctx, id, name, options, position)
}DeleteBoardProperty method · go · L293-L295 (3 LOC)backend/internal/service/service.go
func (s *Service) DeleteBoardProperty(ctx context.Context, id string) error {
return s.repo.BoardProperties().Delete(ctx, id)
}GetMonthStats method · go · L299-L301 (3 LOC)backend/internal/service/service.go
func (s *Service) GetMonthStats(ctx context.Context, year int, month time.Month) ([]storage.DayCount, error) {
return s.repo.Issues().GetMonthStats(ctx, year, month)
}GetDayStats method · go · L303-L305 (3 LOC)backend/internal/service/service.go
func (s *Service) GetDayStats(ctx context.Context, day time.Time) (storage.DayStats, error) {
return s.repo.Issues().GetDayStats(ctx, day)
}CreateWebhook method · go · L17-L37 (21 LOC)backend/internal/service/webhook.go
func (s *Service) CreateWebhook(ctx context.Context, boardID, name, url string, events []domain.WebhookEvent) (domain.Webhook, error) {
if name == "" {
return domain.Webhook{}, errors.New("webhook name must not be empty")
}
if url == "" {
return domain.Webhook{}, errors.New("webhook url must not be empty")
}
for _, e := range events {
if !e.Valid() {
return domain.Webhook{}, errors.New("invalid webhook event: " + string(e))
}
}
wh := domain.Webhook{
BoardID: boardID,
Name: name,
URL: url,
Events: events,
Enabled: true,
}
return s.repo.Webhooks().Create(ctx, wh)
}Source: Repobility analyzer · https://repobility.com
ListWebhooks method · go · L39-L41 (3 LOC)backend/internal/service/webhook.go
func (s *Service) ListWebhooks(ctx context.Context, boardID string) ([]domain.Webhook, error) {
return s.repo.Webhooks().List(ctx, boardID)
}UpdateWebhook method · go · L43-L45 (3 LOC)backend/internal/service/webhook.go
func (s *Service) UpdateWebhook(ctx context.Context, id string, name *string, url *string, events *[]domain.WebhookEvent, enabled *bool) (domain.Webhook, error) {
return s.repo.Webhooks().Update(ctx, id, name, url, events, enabled)
}DeleteWebhook method · go · L47-L49 (3 LOC)backend/internal/service/webhook.go
func (s *Service) DeleteWebhook(ctx context.Context, id string) error {
return s.repo.Webhooks().Delete(ctx, id)
}DispatchWebhook method · go · L53-L91 (39 LOC)backend/internal/service/webhook.go
func (s *Service) DispatchWebhook(boardID string, event domain.WebhookEvent, issue *domain.Issue) {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
hooks, err := s.repo.Webhooks().ListByEvent(ctx, boardID, event)
if err != nil || len(hooks) == 0 {
return
}
board, _ := s.repo.Boards().GetByID(ctx, boardID)
payload := domain.WebhookPayload{
Event: event,
Issue: issue,
Board: &board,
Timestamp: s.now(),
}
body, _ := json.Marshal(payload)
client := &http.Client{Timeout: 5 * time.Second}
for _, wh := range hooks {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, wh.URL, bytes.NewReader(body))
if err != nil {
log.Printf("webhook %s: request error: %v", wh.Name, err)
continue
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Webhook-Event", string(event))
resp, err := client.Do(req)
if err != nil {
log.Printf("webhook %s: sRunAll function · go · L20-L39 (20 LOC)backend/internal/storage/contract/contract.go
func RunAll(t *testing.T, newRepo func(t *testing.T) storage.Repo) {
t.Helper()
cases := []struct {
name string
fn func(t *testing.T, repo storage.Repo)
}{
{"CreateAndGet", testCreateAndGet},
{"ApproveIdempotent", testApproveIdempotent},
{"CycleDetection", testCycleDetection},
{"CascadeDelete", testCascadeDelete},
{"MonthStats", testMonthStats},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
repo := newRepo(t)
tc.fn(t, repo)
})
}
}seedBoard function · go · L41-L53 (13 LOC)backend/internal/storage/contract/contract.go
func seedBoard(t *testing.T, repo storage.Repo) domain.Board {
t.Helper()
b := domain.Board{
ID: uuid.NewString(),
Name: "contract-board",
ViewType: domain.ViewKanban,
CreatedAt: time.Now().UTC(),
}
if _, err := repo.Boards().Create(context.Background(), b); err != nil {
t.Fatalf("seed board: %v", err)
}
return b
}mkIssue function · go · L55-L61 (7 LOC)backend/internal/storage/contract/contract.go
func mkIssue(boardID string) domain.Issue {
now := time.Now().UTC()
return domain.Issue{
ID: uuid.NewString(), BoardID: boardID, Title: "t",
Status: domain.StatusPending, CreatedAt: now, UpdatedAt: now,
}
}testCreateAndGet function · go · L63-L77 (15 LOC)backend/internal/storage/contract/contract.go
func testCreateAndGet(t *testing.T, repo storage.Repo) {
ctx := context.Background()
b := seedBoard(t, repo)
iss := mkIssue(b.ID)
if _, err := repo.Issues().Create(ctx, iss); err != nil {
t.Fatalf("create: %v", err)
}
got, err := repo.Issues().GetByID(ctx, iss.ID)
if err != nil {
t.Fatalf("get: %v", err)
}
if got.Status != domain.StatusPending {
t.Errorf("status: got=%s want=Pending", got.Status)
}
}If a scraper extracted this row, it came from Repobility (https://repobility.com)
testApproveIdempotent function · go · L79-L100 (22 LOC)backend/internal/storage/contract/contract.go
func testApproveIdempotent(t *testing.T, repo storage.Repo) {
ctx := context.Background()
b := seedBoard(t, repo)
iss := mkIssue(b.ID)
if _, err := repo.Issues().Create(ctx, iss); err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
first, err := repo.Issues().Approve(ctx, iss.ID, now)
if err != nil || first.ApprovedAt == nil {
t.Fatalf("approve 1: err=%v approved=%v", err, first.ApprovedAt)
}
firstApproved := *first.ApprovedAt
time.Sleep(15 * time.Millisecond)
second, err := repo.Issues().Approve(ctx, iss.ID, time.Now().UTC())
if err != nil {
t.Fatalf("approve 2: %v", err)
}
if !second.ApprovedAt.Equal(firstApproved) {
t.Fatalf("not idempotent: first=%v second=%v", firstApproved, *second.ApprovedAt)
}
}testCycleDetection function · go · L102-L134 (33 LOC)backend/internal/storage/contract/contract.go
func testCycleDetection(t *testing.T, repo storage.Repo) {
ctx := context.Background()
b := seedBoard(t, repo)
ir := repo.Issues()
create := func(title string) domain.Issue {
i := mkIssue(b.ID)
i.Title = title
if _, err := ir.Create(ctx, i); err != nil {
t.Fatalf("create %s: %v", title, err)
}
return i
}
a, bi, c := create("A"), create("B"), create("C")
setParent := func(child, parent string) error {
p := &parent
pp := &p
_, err := ir.Update(ctx, child, storage.IssuePatch{ParentID: pp})
return err
}
if err := setParent(bi.ID, a.ID); err != nil {
t.Fatalf("B→A: %v", err)
}
if err := setParent(c.ID, bi.ID); err != nil {
t.Fatalf("C→B: %v", err)
}
if err := setParent(a.ID, c.ID); !errors.Is(err, storage.ErrCycle) {
t.Fatalf("cycle A→C expected ErrCycle, got %v", err)
}
if err := setParent(a.ID, a.ID); !errors.Is(err, storage.ErrCycle) {
t.Fatalf("self parent expected ErrCycle, got %v", err)
}
}testCascadeDelete function · go · L136-L156 (21 LOC)backend/internal/storage/contract/contract.go
func testCascadeDelete(t *testing.T, repo storage.Repo) {
ctx := context.Background()
b := seedBoard(t, repo)
ir := repo.Issues()
parent := mkIssue(b.ID)
if _, err := ir.Create(ctx, parent); err != nil {
t.Fatal(err)
}
pid := parent.ID
child := mkIssue(b.ID)
child.ParentID = &pid
if _, err := ir.Create(ctx, child); err != nil {
t.Fatal(err)
}
if err := ir.Delete(ctx, parent.ID); err != nil {
t.Fatal(err)
}
if _, err := ir.GetByID(ctx, child.ID); !errors.Is(err, storage.ErrNotFound) {
t.Fatalf("cascade failed: %v", err)
}
}testMonthStats function · go · L158-L185 (28 LOC)backend/internal/storage/contract/contract.go
func testMonthStats(t *testing.T, repo storage.Repo) {
ctx := context.Background()
b := seedBoard(t, repo)
ir := repo.Issues()
day := time.Now().UTC()
iss := mkIssue(b.ID)
iss.CreatedAt = day
iss.UpdatedAt = day
if _, err := ir.Create(ctx, iss); err != nil {
t.Fatal(err)
}
if _, err := ir.Approve(ctx, iss.ID, day); err != nil {
t.Fatal(err)
}
stats, err := ir.GetMonthStats(ctx, day.Year(), day.Month())
if err != nil {
t.Fatal(err)
}
var seen bool
for _, d := range stats {
if d.Date.Day() == day.Day() && d.Created >= 1 && d.Approved >= 1 {
seen = true
}
}
if !seen {
t.Fatalf("expected created/approved for today, got %+v", stats)
}
}Register function · go · L20-L22 (3 LOC)backend/internal/storage/factory.go
func Register(scheme string, o Opener) {
openers[scheme] = o
}NewRepo function · go · L25-L32 (8 LOC)backend/internal/storage/factory.go
func NewRepo(dsn string) (Repo, error) {
scheme := dsnScheme(dsn)
o, ok := openers[scheme]
if !ok {
return nil, fmt.Errorf("storage: unsupported DSN scheme %q (registered: %v)", scheme, keys(openers))
}
return o(dsn)
}keys function · go · L45-L51 (7 LOC)backend/internal/storage/factory.go
func keys(m map[string]Opener) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}init function · go · L5-L9 (5 LOC)backend/internal/storage/postgres/init.go
func init() {
storage.Register("postgres", func(dsn string) (storage.Repo, error) {
return Open(dsn)
})
}Same scanner, your repo: https://repobility.com — Repobility
runMigrations function · go · L14-L23 (10 LOC)backend/internal/storage/postgres/migrations.go
func runMigrations(db *sql.DB) error {
goose.SetBaseFS(migrationsFS)
if err := goose.SetDialect("postgres"); err != nil {
return fmt.Errorf("postgres: set dialect: %w", err)
}
if err := goose.Up(db, "migrations"); err != nil {
return fmt.Errorf("postgres: goose up: %w", err)
}
return nil
}Open function · go · L28-L46 (19 LOC)backend/internal/storage/postgres/repo.go
func Open(dsn string) (*Repo, error) {
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, fmt.Errorf("postgres open: %w", err)
}
if err := db.Ping(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("postgres ping: %w", err)
}
if err := runMigrations(db); err != nil {
_ = db.Close()
return nil, err
}
return &Repo{
db: db,
issues: &issueRepo{db: db},
boards: &boardRepo{db: db},
}, nil
}Create method · go · L61-L75 (15 LOC)backend/internal/storage/postgres/repo.go
func (r *issueRepo) Create(ctx context.Context, i domain.Issue) (domain.Issue, error) {
if i.Criteria == nil {
i.Criteria = []domain.Criterion{}
}
criteriaJSON, _ := json.Marshal(i.Criteria)
_, err := r.db.ExecContext(ctx, `
INSERT INTO issues (id, board_id, parent_id, title, body, instructions, status, criteria, created_at, updated_at, approved_at, completed_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)`,
i.ID, i.BoardID, i.ParentID, i.Title, i.Body, i.Instructions, i.Status, string(criteriaJSON),
i.CreatedAt.UTC(), i.UpdatedAt.UTC(), utcPtr(i.ApprovedAt), utcPtr(i.CompletedAt))
if err != nil {
return domain.Issue{}, fmt.Errorf("postgres: create issue: %w", err)
}
return i, nil
}GetByID method · go · L77-L79 (3 LOC)backend/internal/storage/postgres/repo.go
func (r *issueRepo) GetByID(ctx context.Context, id string) (domain.Issue, error) {
return scanIssue(r.db.QueryRowContext(ctx, selectIssueCols+` WHERE id = $1`, id))
}ListChildren method · go · L81-L88 (8 LOC)backend/internal/storage/postgres/repo.go
func (r *issueRepo) ListChildren(ctx context.Context, parentID string) ([]domain.Issue, error) {
rows, err := r.db.QueryContext(ctx, selectIssueCols+` WHERE parent_id = $1 ORDER BY created_at ASC`, parentID)
if err != nil {
return nil, fmt.Errorf("postgres: list children: %w", err)
}
defer rows.Close()
return collectIssues(rows)
}List method · go · L90-L120 (31 LOC)backend/internal/storage/postgres/repo.go
func (r *issueRepo) List(ctx context.Context, f storage.IssueFilter) ([]domain.Issue, error) {
q := selectIssueCols + ` WHERE 1=1`
var args []any
i := 1
addArg := func(v any) string {
args = append(args, v)
p := fmt.Sprintf("$%d", i)
i++
return p
}
if f.BoardID != nil {
q += ` AND board_id = ` + addArg(*f.BoardID)
}
if f.Status != nil {
q += ` AND status = ` + addArg(string(*f.Status))
}
if f.ParentID != nil {
if *f.ParentID == "" {
q += ` AND parent_id IS NULL`
} else {
q += ` AND parent_id = ` + addArg(*f.ParentID)
}
}
q += ` ORDER BY position ASC, created_at DESC`
rows, err := r.db.QueryContext(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("postgres: list issues: %w", err)
}
defer rows.Close()
return collectIssues(rows)
}Update method · go · L122-L181 (60 LOC)backend/internal/storage/postgres/repo.go
func (r *issueRepo) Update(ctx context.Context, id string, p storage.IssuePatch) (domain.Issue, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return domain.Issue{}, fmt.Errorf("postgres: begin: %w", err)
}
defer tx.Rollback()
cur, err := scanIssue(tx.QueryRowContext(ctx, selectIssueCols+` WHERE id = $1`, id))
if err != nil {
return domain.Issue{}, err
}
if p.Title != nil {
cur.Title = *p.Title
}
if p.Body != nil {
cur.Body = *p.Body
}
if p.Instructions != nil {
cur.Instructions = *p.Instructions
}
if p.Status != nil {
cur.Status = *p.Status
}
if p.Criteria != nil {
cur.Criteria = *p.Criteria
}
if p.ParentID != nil {
newParent := *p.ParentID
if newParent != nil {
if *newParent == id {
return domain.Issue{}, storage.ErrCycle
}
hasCycle, err := detectCycleTx(ctx, tx, id, *newParent)
if err != nil {
return domain.Issue{}, err
}
if hasCycle {
return domain.Issue{}, storage.ErrCycle
}
cur.ParentID = newParDelete method · go · L183-L193 (11 LOC)backend/internal/storage/postgres/repo.go
func (r *issueRepo) Delete(ctx context.Context, id string) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM issues WHERE id = $1`, id)
if err != nil {
return fmt.Errorf("postgres: delete issue: %w", err)
}
n, _ := res.RowsAffected()
if n == 0 {
return storage.ErrNotFound
}
return nil
}Powered by Repobility — scan your code at https://repobility.com
Approve method · go · L195-L207 (13 LOC)backend/internal/storage/postgres/repo.go
func (r *issueRepo) Approve(ctx context.Context, id string, now time.Time) (domain.Issue, error) {
_, err := r.db.ExecContext(ctx, `
UPDATE issues
SET status='Approved',
approved_at = COALESCE(approved_at, $1),
updated_at = $2
WHERE id = $3 AND status IN ('Pending','Approved')`,
now.UTC(), now.UTC(), id)
if err != nil {
return domain.Issue{}, fmt.Errorf("postgres: approve: %w", err)
}
return r.GetByID(ctx, id)
}Complete method · go · L209-L232 (24 LOC)backend/internal/storage/postgres/repo.go
func (r *issueRepo) Complete(ctx context.Context, id string, now time.Time) (domain.Issue, error) {
res, err := r.db.ExecContext(ctx, `
UPDATE issues
SET status='Done',
completed_at = COALESCE(completed_at, $1),
updated_at = $2
WHERE id = $3 AND status = 'InProgress'`,
now.UTC(), now.UTC(), id)
if err != nil {
return domain.Issue{}, fmt.Errorf("postgres: complete: %w", err)
}
n, _ := res.RowsAffected()
if n == 0 {
cur, err := r.GetByID(ctx, id)
if err != nil {
return domain.Issue{}, err
}
if cur.Status == domain.StatusDone {
return cur, nil
}
return domain.Issue{}, storage.ErrConflict
}
return r.GetByID(ctx, id)
}GetMonthStats method · go · L234-L268 (35 LOC)backend/internal/storage/postgres/repo.go
func (r *issueRepo) GetMonthStats(ctx context.Context, year int, month time.Month) ([]storage.DayCount, error) {
start := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
end := start.AddDate(0, 1, 0)
q := `
WITH days AS (
SELECT (created_at::date) AS d, 1 AS c, 0 AS a, 0 AS f FROM issues
WHERE created_at >= $1 AND created_at < $2
UNION ALL
SELECT (approved_at::date), 0, 1, 0 FROM issues
WHERE approved_at IS NOT NULL AND approved_at >= $1 AND approved_at < $2
UNION ALL
SELECT (completed_at::date), 0, 0, 1 FROM issues
WHERE completed_at IS NOT NULL AND completed_at >= $1 AND completed_at < $2
)
SELECT d, SUM(c)::int, SUM(a)::int, SUM(f)::int
FROM days
GROUP BY d
ORDER BY d`
rows, err := r.db.QueryContext(ctx, q, start, end)
if err != nil {
return nil, fmt.Errorf("postgres: month stats: %w", err)
}
defer rows.Close()
var out []storage.DayCount
for rows.Next() {
var d time.Time
var c storage.DayCount
if err := rows.Scan(&d, &c.Created, &