{"record":{"id":"152fc8b0ecb130b5","repo":"gastownhall/beads","slug":"db-exists-id-must-not-be-empty","errorCode":null,"errorMessage":"db: Exists: id must not be empty","messagePattern":"db: Exists: id must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/issue.go","lineNumber":605,"sourceCode":"\tdefer rows.Close()\n\n\tvar out []*types.Issue\n\tfor rows.Next() {\n\t\tissue, err := scanIssue(rows)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"db: GetByIDs: scan: %w\", err)\n\t\t}\n\t\tout = append(out, issue)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"db: GetByIDs: rows: %w\", err)\n\t}\n\treturn out, nil\n}\n\nfunc (r *issueSQLRepositoryImpl) Exists(ctx context.Context, id string, opts domain.IssueTableOpts) (bool, error) {\n\tif id == \"\" {\n\t\treturn false, errors.New(\"db: Exists: id must not be empty\")\n\t}\n\ttable := pickIssueTable(opts.UseWispsTable)\n\t//nolint:gosec // G201: table is one of two hardcoded constants\n\trow := r.runner.QueryRowContext(ctx, fmt.Sprintf(\"SELECT 1 FROM %s WHERE id = ? LIMIT 1\", table), id)\n\tvar one int\n\terr := row.Scan(&one)\n\tif errors.Is(err, sql.ErrNoRows) {\n\t\treturn false, nil\n\t}\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"db: Exists %s: %w\", id, err)\n\t}\n\treturn true, nil\n}\n\nfunc (r *issueSQLRepositoryImpl) CountForPrefix(ctx context.Context, prefix string, opts domain.IssueTableOpts) (int, error) {\n\tif prefix == \"\" {\n\t\treturn 0, errors.New(\"db: CountForPrefix: prefix must not be empty\")","sourceCodeStart":587,"sourceCodeEnd":623,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/issue.go#L587-L623","documentation":"Exists() validates its issue id before querying the issues table. Because an empty id can never match a row, the repository refuses the query instead of silently returning false, so a caller bug (uninitialized or lost id) surfaces as an explicit error rather than a misleading result.","triggerScenarios":"Calling issueSQLRepositoryImpl.Exists(ctx, \"\", opts) — i.e. any code path that passes an empty/zero-value id, typically from an issue struct whose ID field was never set or from parsing an empty string.","commonSituations":"Constructing an Issue literal without an ID and calling Exists on it; splitting/parsing an ID string like 'bd-' that yields an empty suffix; passing an empty variable after a failed lookup.","solutions":["Ensure the caller populates the id (e.g. from an existing issue) before calling Exists","Guard with `if id == \"\" { return false, nil }` when an empty id legitimately means 'does not exist' in your domain","Verify the ID was not truncated by string splitting (e.g. `strings.TrimPrefix(id, \"bd-\")` on an already-bare id)"],"exampleFix":"// before\nexists, err := repo.Exists(ctx, issue.ID, opts)\n// after\nif issue.ID == \"\" {\n    return fmt.Errorf(\"issue has no id; cannot check existence\")\n}\nexists, err := repo.Exists(ctx, issue.ID, opts)","handlingStrategy":"validation","validationCode":"func requireID(id string) error {\n    if id == \"\" {\n        return errors.New(\"issue id is empty\")\n    }\n    return nil\n}","typeGuard":"func hasID(id string) bool { return strings.TrimSpace(id) != \"\" }","tryCatchPattern":"exists, err := repo.Exists(ctx, id, opts)\nif err != nil {\n    return fmt.Errorf(\"check existence for %q: %w\", id, err)\n}","preventionTips":["Never pass struct fields straight into Exists without checking they are populated","When parsing IDs with TrimPrefix/Split, assert the remainder is non-empty","Make ID generation mandatory at issue-construction time"],"tags":["go","validation","database","empty-id"],"backgroundTag":"empty-required-argument","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}