package datascience import "fmt" // DiffEntities compares two snapshots of entities and returns field-level differences. // Detects added, removed, modified, and unchanged entities. // ignoreFields specifies fields to exclude from comparison (defaults to ["created_at", "updated_at"] when nil). func DiffEntities(before, after []map[string]any, key string, ignoreFields []string) map[string]any { if ignoreFields == nil { ignoreFields = []string{"created_at", "updated_at"} } ignoreSet := make(map[string]bool, len(ignoreFields)) for _, f := range ignoreFields { ignoreSet[f] = true } beforeMap := make(map[string]map[string]any, len(before)) for _, e := range before { if k, ok := e[key]; ok { beforeMap[fmt.Sprintf("%v", k)] = e } } afterMap := make(map[string]map[string]any, len(after)) for _, e := range after { if k, ok := e[key]; ok { afterMap[fmt.Sprintf("%v", k)] = e } } added := []map[string]any{} for k, e := range afterMap { if _, exists := beforeMap[k]; !exists { added = append(added, e) } } removed := []map[string]any{} for k, e := range beforeMap { if _, exists := afterMap[k]; !exists { removed = append(removed, e) } } modified := []map[string]any{} unchanged := 0 for k, b := range beforeMap { a, exists := afterMap[k] if !exists { continue } // Collect all fields from both entities allFields := make(map[string]bool) for f := range b { allFields[f] = true } for f := range a { allFields[f] = true } changes := map[string]any{} for field := range allFields { if ignoreSet[field] || field == key { continue } oldVal := b[field] newVal := a[field] if fmt.Sprintf("%v", oldVal) != fmt.Sprintf("%v", newVal) { changes[field] = map[string]any{"old": oldVal, "new": newVal} } } if len(changes) > 0 { modified = append(modified, map[string]any{"key": k, "changes": changes}) } else { unchanged++ } } nAdded := len(added) nRemoved := len(removed) nModified := len(modified) summary := fmt.Sprintf("%d added, %d removed, %d modified, %d unchanged", nAdded, nRemoved, nModified, unchanged) return map[string]any{ "added": added, "removed": removed, "modified": modified, "unchanged": unchanged, "summary": summary, } }