{"record":{"id":"16fa5339b023de8b","repo":"gastownhall/beads","slug":"unexpected-s-conflict-row-with-no-our-s-safety","errorCode":null,"errorMessage":"unexpected %s conflict row with no our_%s (safety check bypassed)","messagePattern":"unexpected (.+?) conflict row with no our_(.+?) \\(safety check bypassed\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/versioncontrolops/automerge.go","lineNumber":707,"sourceCode":"type unionRowKey struct {\n\tcolumns []string\n\tvalues  []any\n}\n\n// resolveUnionConflicts settles the conflicts unionConflictsAreSafe validated.\n// Both sides hold the same row, so our working set already carries the union:\n// deleting the conflict row is the whole resolution.\nfunc resolveUnionConflicts(ctx context.Context, db DBConn, table string, plan []unionRowKey) error {\n\tif _, ok := unionConflictKeyColumns[table]; !ok {\n\t\treturn fmt.Errorf(\"table %s is not union-mergeable\", table)\n\t}\n\tfor _, row := range plan {\n\t\tpreds := make([]string, 0, len(row.columns))\n\t\targs := make([]any, 0, len(row.columns))\n\t\tfor i, k := range row.columns {\n\t\t\tv := row.values[i]\n\t\t\tif v == nil {\n\t\t\t\treturn fmt.Errorf(\"unexpected %s conflict row with no our_%s (safety check bypassed)\", table, k)\n\t\t\t}\n\t\t\tpreds = append(preds, \"`our_\"+k+\"` = ?\")\n\t\t\targs = append(args, v)\n\t\t}\n\t\t//nolint:gosec // table and key columns come from the unionConflictKeyColumns allowlist.\n\t\tstmt := \"DELETE FROM `dolt_conflicts_\" + table + \"` WHERE \" + strings.Join(preds, \" AND \")\n\t\tres, err := db.ExecContext(ctx, stmt, args...)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"clear %s conflict: %w\", table, err)\n\t\t}\n\t\tif n, err := res.RowsAffected(); err == nil && n == 0 {\n\t\t\treturn fmt.Errorf(\"a %s conflict was not cleared (no conflict row deleted)\", table)\n\t\t}\n\t}\n\treturn nil\n}\n","sourceCodeStart":689,"sourceCodeEnd":724,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/versioncontrolops/automerge.go#L689-L724","documentation":"While building the DELETE for a validated conflict row, a key-column value read from the our_ side is nil. unionRowIsSafe should have guaranteed both sides exist and agree, so a nil here means the safety check was bypassed or the plan was built from stale/malformed data. The function aborts rather than delete with a NULL predicate that could clear the wrong (or no) conflict rows.","triggerScenarios":"A unionRowKey in the plan has a nil entry in values for one of the key columns — e.g. the plan was constructed outside unionConflictsAreSafe, the underlying dolt_conflicts_<table> schema changed between the check pass and resolution, or a schema merge altered the our_<key> columns so the value read back as NULL.","commonSituations":"Concurrent schema merge extended or renamed key columns between loadConflictRows and the DELETE; hand-built plans in tests or new code paths skip unionRowIsSafe; a peer repo's schema merge changed the conflict table's column set.","solutions":["Always build the plan via unionConflictsAreSafe (which calls unionRowIsSafe) — never construct unionRowKey values by hand.","Re-run the merge/auto-resolve so the check pass and resolution pass operate on the same schema snapshot.","Inspect dolt_conflicts_<table> for NULL our_<keycol> values; a NULL key indicates a deeper schema-merge problem to resolve manually.","If schema drift is the cause, settle the schema conflict first, then retry conflict resolution."],"exampleFix":"// before — plan built without validation\n plan := []unionRowKey{{columns: []string{\"issue_id\", \"label\"}, values: []any{nil, \"bug\"}}}\n resolveUnionConflicts(ctx, db, \"labels\", plan)\n// after\n plan, safe, err := unionConflictsAreSafe(ctx, db, \"labels\")\n if err != nil { return err }\n if !safe { return errors.New(\"conflicts not auto-resolvable\") }\n resolveUnionConflicts(ctx, db, \"labels\", plan)","handlingStrategy":"validation","validationCode":"// validate plan keys before resolving\nfor _, row := range plan {\n\tfor i, v := range row.values {\n\t\tif v == nil {\n\t\t\treturn fmt.Errorf(\"plan row %d has nil key value for %s\", i, row.columns[i])\n\t\t}\n\t}\n}","typeGuard":"func planKeysPresent(plan []unionRowKey) bool {\n\tfor _, row := range plan {\n\t\tfor _, v := range row.values {\n\t\t\tif v == nil { return false }\n\t\t}\n\t}\n\treturn true\n}","tryCatchPattern":null,"preventionTips":["Build plans exclusively through unionConflictsAreSafe/unionRowIsSafe.","Re-run the check pass and resolution atomically against the same schema snapshot.","Resolve schema (column-set) conflicts before row conflict resolution.","Never hand-construct unionRowKey values in production code."],"tags":["go","merge-conflicts","invariant-violation","dolt"],"backgroundTag":"safety-check-bypassed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}