{"record":{"id":"79c94d041f699b6c","repo":"gastownhall/beads","slug":"record-event-in-s-w","errorCode":null,"errorMessage":"record event in %s: %w","messagePattern":"record event in (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/derivedid.go","lineNumber":158,"sourceCode":"\t\tvar id string\n\t\tif err := rows.Scan(&id); err != nil {\n\t\t\t_ = rows.Close()\n\t\t\treturn \"\", fmt.Errorf(\"scan same-content events in %s: %w\", table, err)\n\t\t}\n\t\ttaken[id] = true\n\t}\n\t_ = rows.Close()\n\tif err := rows.Err(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"scan same-content events in %s: %w\", table, err)\n\t}\n\n\tid := firstFreeDerivedID(table, digest, taken)\n\tif _, err := tx.ExecContext(ctx, fmt.Sprintf(`\n\t\tINSERT INTO %s (id, issue_id, event_type, actor, old_value, new_value, comment, created_at)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?)`, table),\n\t\tid,\n\t\te.IssueID, string(e.EventType), e.Actor, e.OldValue, e.NewValue, e.Comment, e.CreatedAt); err != nil {\n\t\treturn \"\", fmt.Errorf(\"record event in %s: %w\", table, err)\n\t}\n\treturn id, nil\n}\n\n// NextLiveCommentTime returns the created_at to stamp on a comment being added\n// live (as opposed to imported), given the wall-clock instant the caller\n// observed. The result is always truncated to whole seconds — the created_at\n// column's DATETIME(0) precision — and is advanced to one second past the\n// issue's newest existing comment when that comment is at or after `now`.\n//\n// Why: comments read back in (created_at ASC, id ASC) order, created_at holds\n// whole seconds, and since bd-ri8bd a comment's id is a content digest rather\n// than a time-ordered UUIDv7. Two comments added to one issue inside the same\n// wall-clock second therefore tie on the primary sort key and then order by\n// hash — arbitrarily with respect to the order they were written. Keeping\n// (issue_id, created_at) unique on the live path is what restores insertion\n// order for the reader without putting ordering information into the id, which\n// content-derivation cannot carry.","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/derivedid.go#L140-L176","documentation":"Wraps the INSERT that records the derived event row under its content-derived id in InsertDerivedEventReturningID. ExecContext failure here means the row could not be written — duplicate id (content collision with different disposition), constraint violation, missing table, or transaction already aborted. Callers AddCommentEventInTx and InsertDerivedEvent propagate it.","triggerScenarios":"Two events compute the same derived id via firstFreeDerivedID but race within conflicting transactions; the events table lacks expected columns; the transaction was already rolled back by an earlier error; UNIQUE/PK violation on id.","commonSituations":"Concurrent writers inserting identical comments through separate processes against the same Dolt DB; schema drift; long-running transactions that get killed server-side before the INSERT.","solutions":["Retry the operation in a fresh transaction — if the conflicting id now exists, the dedup SELECT will see it and pick the next free ordinal.","Serialize concurrent writers on the same issue (bd's locking) instead of racing inserts from multiple processes.","Verify the table exists with the expected 8 columns; run migrations if not.","Inspect the wrapped driver error to distinguish PK-duplicate from connection/abort causes."],"exampleFix":"// before: racing inserts collide on derived id\nif _, err := tx.ExecContext(ctx, insertSQL, id, ...); err != nil {\n\treturn \"\", fmt.Errorf(\"record event in %s: %w\", table, err)\n}\n// after: re-read taken set and pick next free id on duplicate-key\nif _, err := tx.ExecContext(ctx, insertSQL, id, ...); err != nil {\n\tif isDuplicateKey(err) {\n\t\ttaken[id] = true\n\t\tid = firstFreeDerivedID(table, digest, taken)\n\t\t_, err = tx.ExecContext(ctx, insertSQL, id, ...)\n\t}\n\tif err != nil { return \"\", fmt.Errorf(\"record event in %s: %w\", table, err) }\n}","handlingStrategy":"retry","validationCode":"cols, err := tableColumns(tx, table)\nif err != nil { return err }\nif len(cols) != 8 {\n\treturn fmt.Errorf(\"%s expects 8 columns, found %d; migrate\", table, len(cols))\n}\nif err := ctx.Err(); err != nil { return err }","typeGuard":"func isDuplicateKeyErr(err error) bool {\n\tvar me *mysql.MySQLError\n\treturn errors.As(err, &me) && me.Number == 1062\n}","tryCatchPattern":"id, err := InsertDerivedEventReturningID(ctx, tx, table, e)\nif err != nil {\n\tif isDuplicateKeyErr(err) {\n\t\t// benign race: retry in a fresh tx; dedup SELECT will see the row\n\t\treturn insertInFreshTx(ctx, table, e)\n\t}\n\treturn err\n}","preventionTips":["Avoid multiple processes writing identical events concurrently; use bd's locking.","Keep transactions short so they aren't reaped mid-INSERT.","Migrate schemas before bulk import runs.","Treat duplicate-key on derived ids as dedup-won, not corruption."],"tags":["sql","insert-error","storage","derived-events"],"backgroundTag":"sql-insert-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}