gastownhall/beads · error

scan parent-child dep from %s: %w

Error message

scan parent-child dep from %s: %w

What it means

Each dependency row is scanned into two strings (parentID, childID). A Scan failure aborts with the table name included. This means a row in the parent-child dependency result does not have the expected two-string shape, so dependency graph construction stops.

Source

Thrown at internal/storage/issueops/epic_closure.go:60

	for _, id := range epicIDs {
		epicSet[id] = true
	}
	for _, depTable := range []string{"dependencies", "wisp_dependencies"} {
		depRows, err := tx.QueryContext(ctx, fmt.Sprintf(`
			SELECT %s AS parent_id, issue_id FROM %s
			WHERE type = 'parent-child' AND %s IS NOT NULL
		`, DepTargetExpr, depTable, DepTargetExpr))
		if err != nil {
			if optionalBlockedTable(depTable) && isTableNotExistError(err) {
				continue // wisp_dependencies may not exist on pre-migration databases
			}
			return nil, fmt.Errorf("failed to get parent-child deps from %s: %w", depTable, err)
		}
		for depRows.Next() {
			var parentID, childID string
			if err := depRows.Scan(&parentID, &childID); err != nil {
				depRows.Close()
				return nil, fmt.Errorf("scan parent-child dep from %s: %w", depTable, err)
			}
			if epicSet[parentID] {
				epicChildMap[parentID] = append(epicChildMap[parentID], childID)
			}
		}
		depRows.Close()
	}

	// Step 3: Batch-fetch statuses for all child issues across all epics
	allChildIDs := make([]string, 0)
	for _, children := range epicChildMap {
		allChildIDs = append(allChildIDs, children...)
	}
	childStatusMap := make(map[string]string)
	if len(allChildIDs) > 0 {
		// Check both issues and wisps tables for child statuses (bd-w2w)
		for _, table := range []string{"issues", "wisps"} {
			for start := 0; start < len(allChildIDs); start += queryBatchSize {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Find offending rows: `SELECT * FROM <table> WHERE type='parent-child' AND (<target-col> IS NULL)` and fix or delete them.
  2. Verify DepTargetExpr columns (issue_id/target) are populated for parent-child deps.
  3. Re-import or repair the dependency data from a known-good export.
  4. Update bd if the driver version changed column types.

Example fix

// before
DELETE FROM dependencies WHERE id = 'broken-row';
// after
UPDATE dependencies SET target_id = '<correct-id>' WHERE id = 'broken-row';
Defensive patterns

Strategy: validation

Validate before calling

rows, _ := db.Query("SELECT issue_id, target FROM dependencies WHERE type='parent-child'")
for rows.Next() {
    var p, c sql.NullString
    _ = rows.Scan(&p, &c)
    if !p.Valid || !c.Valid { return fmt.Errorf("parent-child dep with NULL endpoint") }
}

Type guard

func isDepScanErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "scan parent-child dep from")
}

Try / catch

epics, err := GetEpicsEligibleForClosureInTx(ctx, tx)
if err != nil {
    if isDepScanErr(err) { return repairDependencyRowsThenRetry(ctx) }
    return err
}

Prevention

When it happens

Trigger: Calling GetEpicsEligibleForClosureInTx when a row from the parent-child dependency query yields NULL or non-convertible values for parent_id/child_id (via DepTargetExpr columns).

Common situations: Rows where the dep target expression evaluates to NULL for a 'parent-child' type; hand-edited or imported dependency rows with missing IDs; driver type-conversion quirks.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/14df87f5a1586330. Report an issue: GitHub.