{"record":{"id":"5aa1dcbc38d582c9","repo":"gastownhall/beads","slug":"count-open-children-unsupported-target-column-q","errorCode":null,"errorMessage":"count open children: unsupported target column %q","messagePattern":"count open children: unsupported target column %q","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/close.go","lineNumber":195,"sourceCode":"\t\treturn 0, err\n\t}\n\tif !found {\n\t\treturn 0, fmt.Errorf(\"%w: issue %s\", storage.ErrNotFound, id)\n\t}\n\treturn enforceClosePolicyForTargetInTx(ctx, tx, id, targetColumn, force, closed)\n}\n\nfunc countOpenChildrenInTx(ctx context.Context, tx DBTX, id string) (int, error) {\n\ttargetColumn, err := dependencyTargetColumnForIDInTx(ctx, tx, id)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn countOpenChildrenForTargetInTx(ctx, tx, id, targetColumn)\n}\n\nfunc countOpenChildrenForTargetInTx(ctx context.Context, tx DBTX, id, targetColumn string) (int, error) {\n\tif targetColumn != \"depends_on_issue_id\" && targetColumn != \"depends_on_wisp_id\" {\n\t\treturn 0, fmt.Errorf(\"count open children: unsupported target column %q\", targetColumn)\n\t}\n\tvar durableCount int\n\t//nolint:gosec // G201: targetColumn is validated above against two hardcoded identifiers.\n\tdurableQuery := fmt.Sprintf(`\n\t\tSELECT COUNT(DISTINCT dependency.issue_id)\n\t\tFROM dependencies AS dependency\n\t\tJOIN issues AS child ON child.id = dependency.issue_id\n\t\tWHERE dependency.%s = ?\n\t\t  AND dependency.type = 'parent-child'\n\t\t  AND child.status != 'closed'\n\t`, targetColumn)\n\tif err := tx.QueryRowContext(ctx, durableQuery, id).Scan(&durableCount); err != nil {\n\t\treturn 0, fmt.Errorf(\"count open durable children for %s: %w\", id, err)\n\t}\n\n\tvar wispCount int\n\t//nolint:gosec // G201: targetColumn is validated above against two hardcoded identifiers.\n\twispQuery := fmt.Sprintf(`","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L177-L213","documentation":"Beads throws this when a close-policy child count is requested against a parent-dependency column other than the two supported ones: depends_on_issue_id (durable issues) or depends_on_wisp_id (wisps). The column is interpolated into the COUNT query, so it is strictly validated against these two hardcoded identifiers before use. This is an internal invariant guard, not a user-facing validation error.","triggerScenarios":"Calling countOpenChildrenForTargetInTx (or countOpenChildrenInTx / enforceClosePolicyForTargetInTx above it) with a targetColumn string other than 'depends_on_issue_id' or 'depends_on_wisp_id' — i.e. a caller-produced column name from a classifier that returned an unexpected value.","commonSituations":"A fork or local patch added a third dependency table/column and routed it through the close policy; a classifier function was modified to return an empty string or a renamed column; tests injecting a bogus column value to assert the guard fires.","solutions":["Ensure the value passed as targetColumn comes only from dependencyTargetColumnForIDInTx or the hardcoded literals \"depends_on_issue_id\"/\"depends_on_wisp_id\"","Check for empty-string columns caused by an early-return or error path in the classifier being swallowed upstream","If you added a new dependency column, extend the guard in internal/storage/issueops/close.go:194 to whitelist it (and audit the fmt.Sprintf query for injection safety)"],"exampleFix":"// before\nerr := countOpenChildrenForTargetInTx(ctx, tx, id, \"parent_id\") // panics into guard\n// after\ncol := \"depends_on_issue_id\"\nif isWispID(id) { col = \"depends_on_wisp_id\" }\nerr := countOpenChildrenForTargetInTx(ctx, tx, id, col)","handlingStrategy":"validation","validationCode":"func validTargetColumn(col string) bool {\n\treturn col == \"depends_on_issue_id\" || col == \"depends_on_wisp_id\"\n}\nif !validTargetColumn(targetColumn) {\n\treturn fmt.Errorf(\"refusing to count children with column %q\", targetColumn)\n}","typeGuard":"func isDependencyTargetColumn(s string) bool {\n\treturn s == \"depends_on_issue_id\" || s == \"depends_on_wisp_id\"\n}","tryCatchPattern":null,"preventionTips":["Always source targetColumn from dependencyTargetColumnForIDInTx, never from user input or config","Never construct column names with fmt.Sprintf from dynamic data other than the two whitelisted literals","Add a unit test asserting the guard rejects arbitrary strings","When adding new dependency columns, update the whitelist and audit query interpolation together"],"tags":["sql","internal-invariant","dependency-tracking"],"backgroundTag":"unsupported-target-column","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}