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 len(duplicateKeyMessages(err)) > 0 { 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 } 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 } } 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 }