{"record":{"id":"6e646dda53f8c554","repo":"gastownhall/beads","slug":"scan-dependency-conflict-w","errorCode":null,"errorMessage":"scan dependency conflict: %w","messagePattern":"scan dependency conflict: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/versioncontrolops/mergesettle.go","lineNumber":625,"sourceCode":"\t\tFROM dolt_conflicts_dependencies`)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"query dependency conflicts: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar (\n\t\t\tourID, theirID             sql.NullString\n\t\t\tourIssue, theirIssue       sql.NullString\n\t\t\tourDepIssue, theirDepIssue sql.NullString\n\t\t\tourDepWisp, theirDepWisp   sql.NullString\n\t\t\tourDepExt, theirDepExt     sql.NullString\n\t\t\tourType, theirType         sql.NullString\n\t\t)\n\t\tif err := rows.Scan(&ourID, &theirID, &ourIssue, &theirIssue,\n\t\t\t&ourDepIssue, &theirDepIssue, &ourDepWisp, &theirDepWisp,\n\t\t\t&ourDepExt, &theirDepExt, &ourType, &theirType); err != nil {\n\t\t\treturn false, fmt.Errorf(\"scan dependency conflict: %w\", err)\n\t\t}\n\t\t// One side deleted the edge (add/delete conflict): leave for the operator.\n\t\tif !ourID.Valid || !theirID.Valid {\n\t\t\treturn false, nil\n\t\t}\n\t\t// Same edge requires the same source issue. A differing issue_id means the\n\t\t// shared id is stale on one side (e.g. a rename), not a shared edge.\n\t\tif ourIssue.Valid != theirIssue.Valid || ourIssue.String != theirIssue.String {\n\t\t\treturn false, nil\n\t\t}\n\t\t// ...and the same resolved target.\n\t\tourTarget, ourOK := resolveConflictDepTarget(ourDepIssue, ourDepWisp, ourDepExt)\n\t\ttheirTarget, theirOK := resolveConflictDepTarget(theirDepIssue, theirDepWisp, theirDepExt)\n\t\tif ourOK != theirOK || ourTarget != theirTarget {\n\t\t\treturn false, nil\n\t\t}\n\t\t// A differing type is the only remaining way this is a real semantic conflict.\n\t\tif ourType.Valid != theirType.Valid || ourType.String != theirType.String {","sourceCodeStart":607,"sourceCodeEnd":643,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/versioncontrolops/mergesettle.go#L607-L643","documentation":"While iterating rows of dolt_conflicts_dependencies, rows.Scan into 12 sql.NullString columns (both sides of issue/wisp/external dependency fields plus type) failed. Scan errors mean the result set shape or types do not match the scan targets — typically a column-count mismatch or an unconvertible type. The routine returns false so conflicting rows are left for operator review.","triggerScenarios":"dependencyConflictsAreAuditOnly reads a row whose column count differs from the 12 expected values (Dolt schema drift), or a column type (e.g. integer/boolean instead of string) cannot scan into sql.NullString.","commonSituations":"Dolt version upgrade changed dolt_conflicts_dependencies columns (added/removed/reordered); a column became non-nullable-typed (e.g. BIGINT) and the driver refuses to convert into NullString; a driver that returns different types for conflict tables than for normal tables.","solutions":["Run DESCRIBE dolt_conflicts_dependencies and match the SELECT column list and order exactly to the 12 Scan targets.","Upgrade or align the Dolt/driver version so the conflict-table schema matches what the query expects.","Scan into sql.RawBytes or interface{} where types are uncertain, then convert explicitly instead of relying on NullString conversion.","Check the wrapped inner error: 'sql: expected N destination arguments' means a count mismatch; 'converting driver.Value type' means a type mismatch."],"exampleFix":"// before\nif err := rows.Scan(&ourID, &theirID, &ourIssue, &theirIssue,\n\t&ourDepIssue, &theirDepIssue, &ourDepWisp, &theirDepWisp,\n\t&ourDepExt, &theirDepExt, &ourType, &theirType); err != nil {\n\treturn false, fmt.Errorf(\"scan dependency conflict: %w\", err)\n}\n// after\ncells := make([]any, 12)\nptrs := make([]any, 12)\nfor i := range cells {\n\tptrs[i] = &cells[i]\n}\nif err := rows.Scan(ptrs...); err != nil {\n\treturn false, fmt.Errorf(\"scan dependency conflict: %w\", err)\n}\ntoNull := func(v any) sql.NullString {\n\ts, _ := v.(string)\n\tif v == nil { return sql.NullString{} }\n\treturn sql.NullString{String: fmt.Sprintf(\"%v\", v), Valid: true}\n}\nourID, theirID := toNull(cells[0]), toNull(cells[1])","handlingStrategy":"type-guard","validationCode":"cols, err := rows.Columns()\nif err != nil || len(cols) != 12 {\n\t// schema drift: do not scan; fall back to operator review\n}","typeGuard":"func scanDependencyConflictRow(rows *sql.Rows) (ok bool) {\n\tcols, err := rows.Columns()\n\tif err != nil || len(cols) != 12 {\n\t\treturn false\n\t}\n\tcells := make([]any, len(cols))\n\tptrs := make([]any, len(cols))\n\tfor i := range cells {\n\t\tptrs[i] = &cells[i]\n\t}\n\treturn rows.Scan(ptrs...) == nil\n}","tryCatchPattern":"auditOnly, err := dependencyConflictsAreAuditOnly(ctx, db)\nif err != nil && strings.Contains(err.Error(), \"scan dependency conflict\") {\n\t// schema mismatch: log and treat conflicts as operator-resolvable\n\tauditOnly = false\n}","preventionTips":["Validate rows.Columns() length before scanning; it is the cheapest schema-drift check.","Scan into interface{} / sql.RawBytes when column types may vary across Dolt versions.","Add a startup schema assertion (DESCRIBE dolt_conflicts_dependencies) so drift is caught before merges run.","On any scan failure, default to leaving conflicts for the operator — never auto-resolve partially parsed rows."],"tags":["dolt","sql","scan","schema-mismatch"],"backgroundTag":"sql-scan-column-mismatch","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}