Enhance duplicate key error handling in MongoDB operations

- Improved the `IsDuplicateKeyError` function to utilize structured error messages for better accuracy in identifying duplicate key errors.
- Introduced `duplicateKeyMessages` and `hasStructuredWriteException` functions to parse and extract relevant information from MongoDB error messages.
- Updated `DuplicateKeyMatchesField` to match duplicate key errors against specific fields by analyzing error messages rather than relying on substring matching.

This refactor enhances the clarity and reliability of duplicate key error handling, ensuring more precise identification of conflicts in MongoDB operations.
This commit is contained in:
Mazyar
2026-06-07 14:40:42 +03:30
parent 550fd36db1
commit 3f0935ee3f
2 changed files with 471 additions and 20 deletions
+168 -20
View File
@@ -2,43 +2,191 @@ package mongo
import (
"errors"
"regexp"
"strings"
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
)
var dupKeyFieldNamePattern = regexp.MustCompile(`(?:^|[,{]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:`)
// IsDuplicateKeyError reports whether err is a MongoDB E11000 duplicate key error.
func IsDuplicateKeyError(err error) bool {
if err == nil {
return false
}
if strings.Contains(err.Error(), "E11000") || strings.Contains(err.Error(), "duplicate key") {
if len(duplicateKeyMessages(err)) > 0 {
return true
}
var we mongodriver.WriteException
if errors.As(err, &we) {
for _, wErr := range we.WriteErrors {
if wErr.Code == 11000 {
return true
}
if hasStructuredWriteException(err) {
return false
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "e11000") || strings.Contains(msg, "duplicate key")
}
// DuplicateKeyMatchesField reports whether a duplicate key error involves the given field
// by parsing WriteException messages (index name and dup key document keys), not broad
// substring matching on the full error text.
func DuplicateKeyMatchesField(err error, field string) bool {
if err == nil || field == "" {
return false
}
field = strings.ToLower(field)
msgs := duplicateKeyMessages(err)
if len(msgs) == 0 {
if hasStructuredWriteException(err) {
return false
}
if !IsDuplicateKeyError(err) {
return false
}
msgs = []string{err.Error()}
}
for _, msg := range msgs {
if duplicateKeyMessageMatchesField(msg, field) {
return true
}
}
return false
}
// DuplicateKeyMatchesField reports whether a duplicate key error involves the given field
// (e.g. "phone" matches index phone_idx or dup key { phone: "..." }).
func DuplicateKeyMatchesField(err error, field string) bool {
if err == nil || field == "" {
func duplicateKeyMessages(err error) []string {
var msgs []string
var writeException mongodriver.WriteException
if errors.As(err, &writeException) {
for _, writeErr := range writeException.WriteErrors {
if writeErr.Code == 11000 {
msgs = append(msgs, writeErr.Message)
}
}
}
var bulkWriteException mongodriver.BulkWriteException
if errors.As(err, &bulkWriteException) {
for _, writeErr := range bulkWriteException.WriteErrors {
if writeErr.Code == 11000 {
msgs = append(msgs, writeErr.Message)
}
}
}
return msgs
}
func hasStructuredWriteException(err error) bool {
var writeException mongodriver.WriteException
if errors.As(err, &writeException) {
return true
}
var bulkWriteException mongodriver.BulkWriteException
return errors.As(err, &bulkWriteException)
}
func duplicateKeyMessageMatchesField(message, field string) bool {
message = strings.ToLower(message)
if indexName := extractDuplicateIndexName(message); indexName != "" {
if indexNameMatchesField(indexName, field) {
return true
}
}
for _, key := range extractDuplicateKeyFields(message) {
if key == field {
return true
}
}
return false
}
func extractDuplicateIndexName(message string) string {
const prefix = "index: "
start := strings.Index(message, prefix)
if start < 0 {
return ""
}
rest := strings.TrimSpace(message[start+len(prefix):])
if rest == "" {
return ""
}
end := strings.IndexByte(rest, ' ')
if end >= 0 {
return rest[:end]
}
return rest
}
func indexNameMatchesField(indexName, field string) bool {
switch indexName {
case field, field + "_idx", field + "_1":
return true
default:
return false
}
msg := strings.ToLower(err.Error())
field = strings.ToLower(field)
if strings.Contains(msg, field+"_idx") {
return true
}
if strings.Contains(msg, "dup key") && strings.Contains(msg, field) {
return true
}
return strings.Contains(msg, `"`+field+`"`) || strings.Contains(msg, field+":")
}
func extractDuplicateKeyFields(message string) []string {
const prefix = "dup key:"
start := strings.Index(message, prefix)
if start < 0 {
return nil
}
document := strings.TrimSpace(message[start+len(prefix):])
if !strings.HasPrefix(document, "{") {
return nil
}
depth := 0
end := -1
for i, char := range document {
switch char {
case '{':
depth++
case '}':
depth--
if depth == 0 {
end = i
}
}
if end >= 0 {
break
}
}
if end <= 0 {
return nil
}
inner := strings.TrimSpace(document[1:end])
matches := dupKeyFieldNamePattern.FindAllStringSubmatch(inner, -1)
if len(matches) == 0 {
return nil
}
fields := make([]string, 0, len(matches))
seen := make(map[string]struct{}, len(matches))
for _, match := range matches {
if len(match) < 2 {
continue
}
key := strings.ToLower(match[1])
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
fields = append(fields, key)
}
return fields
}