Files
tm_back/pkg/mongo/duplicate.go
T
Mazyar 3f0935ee3f 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.
2026-06-07 14:40:42 +03:30

193 lines
3.9 KiB
Go

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
}