3f0935ee3f
- 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.
193 lines
3.9 KiB
Go
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
|
|
}
|