// Package merge provides APOC merge operations.
//
// This package implements all apoc.merge.* functions for merging
// nodes and relationships in graph operations.
package merge
import (
"fmt"
)
// Node represents a graph node.
type Node struct {
ID int64
Labels []string
Properties map[string]interface{}
}
// Relationship represents a graph relationship.
type Relationship struct {
ID int64
Type string
StartNode int64
EndNode int64
Properties map[string]interface{}
}
// MergeNode merges or creates a node based on properties.
//
// Example:
//
// apoc.merge.node(['Person'], {name: 'Alice'}, {age: 30}, {}) => node
func MergeNode(labels []string, identProps, onCreateProps, onMatchProps map[string]interface{}) *Node {
// Search for existing node with identProps
// If found, apply onMatchProps
// If not found, create with identProps + onCreateProps
node := &Node{
ID: generateID(),
Labels: labels,
Properties: make(map[string]interface{}),
}
// Merge all properties
for k, v := range identProps {
node.Properties[k] = v
}
for k, v := range onCreateProps {
node.Properties[k] = v
}
return node
}
// NodeEager eagerly merges a node (evaluates all properties upfront).
//
// Example:
//
// apoc.merge.nodeEager(['Person'], {name: 'Alice'}, {age: 30}) => node
func NodeEager(labels []string, identProps, props map[string]interface{}) *Node {
return MergeNode(labels, identProps, props, nil)
}
// MergeRelationship merges or creates a relationship.
//
// Example:
//
// apoc.merge.relationship(start, 'KNOWS', {}, {since: 2020}, {}, end) => rel
func MergeRelationship(start *Node, relType string, identProps, onCreateProps, onMatchProps map[string]interface{}, end *Node) *Relationship {
rel := &Relationship{
ID: generateID(),
Type: relType,
StartNode: start.ID,
EndNode: end.ID,
Properties: make(map[string]interface{}),
}
// Merge all properties
for k, v := range identProps {
rel.Properties[k] = v
}
for k, v := range onCreateProps {
rel.Properties[k] = v
}
return rel
}
// RelationshipEager eagerly merges a relationship.
//
// Example:
//
// apoc.merge.relationshipEager(start, 'KNOWS', {}, {since: 2020}, end) => rel
func RelationshipEager(start *Node, relType string, identProps, props map[string]interface{}, end *Node) *Relationship {
return MergeRelationship(start, relType, identProps, props, nil, end)
}
// Nodes merges multiple nodes at once.
//
// Example:
//
// apoc.merge.nodes([{labels: ['Person'], props: {name: 'Alice'}}]) => nodes
func Nodes(nodeSpecs []map[string]interface{}) []*Node {
nodes := make([]*Node, 0, len(nodeSpecs))
for _, spec := range nodeSpecs {
labels := []string{}
if l, ok := spec["labels"].([]string); ok {
labels = l
}
props := make(map[string]interface{})
if p, ok := spec["props"].(map[string]interface{}); ok {
props = p
}
node := MergeNode(labels, props, nil, nil)
nodes = append(nodes, node)
}
return nodes
}
// Properties merges properties onto a node.
//
// Example:
//
// apoc.merge.properties(node, {age: 31, city: 'NYC'}) => updated node
func Properties(node *Node, props map[string]interface{}) *Node {
for k, v := range props {
node.Properties[k] = v
}
return node
}
// DeepMerge deeply merges nested properties.
//
// Example:
//
// apoc.merge.deepMerge(node, {address: {city: 'NYC'}}) => updated node
func DeepMerge(node *Node, props map[string]interface{}) *Node {
for k, v := range props {
if existing, ok := node.Properties[k]; ok {
// If both are maps, merge recursively
if existingMap, ok1 := existing.(map[string]interface{}); ok1 {
if newMap, ok2 := v.(map[string]interface{}); ok2 {
node.Properties[k] = mergeMaps(existingMap, newMap)
continue
}
}
}
node.Properties[k] = v
}
return node
}
// mergeMaps recursively merges two maps.
func mergeMaps(m1, m2 map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range m1 {
result[k] = v
}
for k, v := range m2 {
if existing, ok := result[k]; ok {
if existingMap, ok1 := existing.(map[string]interface{}); ok1 {
if newMap, ok2 := v.(map[string]interface{}); ok2 {
result[k] = mergeMaps(existingMap, newMap)
continue
}
}
}
result[k] = v
}
return result
}
// Labels merges labels onto a node.
//
// Example:
//
// apoc.merge.labels(node, ['Employee', 'Manager']) => updated node
func Labels(node *Node, labels []string) *Node {
for _, label := range labels {
if !hasLabel(node, label) {
node.Labels = append(node.Labels, label)
}
}
return node
}
// hasLabel checks if a node has a label.
func hasLabel(node *Node, label string) bool {
for _, l := range node.Labels {
if l == label {
return true
}
}
return false
}
// Pattern merges a pattern (nodes and relationships).
//
// Example:
//
// apoc.merge.pattern(startProps, relType, relProps, endProps) => pattern
func Pattern(startProps map[string]interface{}, relType string, relProps, endProps map[string]interface{}) map[string]interface{} {
startLabels := extractLabels(startProps)
endLabels := extractLabels(endProps)
start := MergeNode(startLabels, startProps, nil, nil)
end := MergeNode(endLabels, endProps, nil, nil)
rel := MergeRelationship(start, relType, relProps, nil, nil, end)
return map[string]interface{}{
"start": start,
"relationship": rel,
"end": end,
}
}
// extractLabels extracts labels from properties map.
func extractLabels(props map[string]interface{}) []string {
if labels, ok := props["_labels"].([]string); ok {
return labels
}
return []string{}
}
// Batch merges multiple entities in a batch.
//
// Example:
//
// apoc.merge.batch(specs, 1000) => results
func Batch(specs []map[string]interface{}, batchSize int) []interface{} {
results := make([]interface{}, 0)
for i := 0; i < len(specs); i += batchSize {
end := i + batchSize
if end > len(specs) {
end = len(specs)
}
batch := specs[i:end]
for _, spec := range batch {
// Process each spec
results = append(results, spec)
}
}
return results
}
// Conditional merges based on a condition.
//
// Example:
//
// apoc.merge.conditional(node, condition, props) => updated node
func Conditional(node *Node, condition bool, props map[string]interface{}) *Node {
if condition {
return Properties(node, props)
}
return node
}
// Strategy applies a merge strategy.
//
// Example:
//
// apoc.merge.strategy(node, props, 'overwrite') => updated node
func Strategy(node *Node, props map[string]interface{}, strategy string) *Node {
switch strategy {
case "overwrite":
return Properties(node, props)
case "keep":
// Only add properties that don't exist
for k, v := range props {
if _, exists := node.Properties[k]; !exists {
node.Properties[k] = v
}
}
return node
case "merge":
return DeepMerge(node, props)
default:
return Properties(node, props)
}
}
// Conflict resolves merge conflicts.
//
// Example:
//
// apoc.merge.conflict(node, props, resolver) => updated node
func Conflict(node *Node, props map[string]interface{}, resolver func(old, new interface{}) interface{}) *Node {
for k, newVal := range props {
if oldVal, exists := node.Properties[k]; exists {
node.Properties[k] = resolver(oldVal, newVal)
} else {
node.Properties[k] = newVal
}
}
return node
}
// Validate validates merge operations.
//
// Example:
//
// apoc.merge.validate(node, props) => {valid: true, errors: []}
func Validate(node *Node, props map[string]interface{}) map[string]interface{} {
errors := make([]string, 0)
// Validate property types
for k, v := range props {
if existing, ok := node.Properties[k]; ok {
if fmt.Sprintf("%T", existing) != fmt.Sprintf("%T", v) {
errors = append(errors, fmt.Sprintf("Type mismatch for property %s", k))
}
}
}
return map[string]interface{}{
"valid": len(errors) == 0,
"errors": errors,
}
}
// Preview previews merge results without applying them.
//
// Example:
//
// apoc.merge.preview(node, props) => preview
func Preview(node *Node, props map[string]interface{}) map[string]interface{} {
preview := &Node{
ID: node.ID,
Labels: make([]string, len(node.Labels)),
Properties: make(map[string]interface{}),
}
copy(preview.Labels, node.Labels)
for k, v := range node.Properties {
preview.Properties[k] = v
}
for k, v := range props {
preview.Properties[k] = v
}
return map[string]interface{}{
"before": node,
"after": preview,
}
}
// Rollback rolls back a merge operation.
//
// Example:
//
// apoc.merge.rollback(node, snapshot) => restored node
func Rollback(node *Node, snapshot map[string]interface{}) *Node {
if props, ok := snapshot["properties"].(map[string]interface{}); ok {
node.Properties = props
}
if labels, ok := snapshot["labels"].([]string); ok {
node.Labels = labels
}
return node
}
// Snapshot creates a snapshot of a node for rollback.
//
// Example:
//
// apoc.merge.snapshot(node) => snapshot
func Snapshot(node *Node) map[string]interface{} {
props := make(map[string]interface{})
for k, v := range node.Properties {
props[k] = v
}
labels := make([]string, len(node.Labels))
copy(labels, node.Labels)
return map[string]interface{}{
"properties": props,
"labels": labels,
}
}
var idCounter int64 = 0
func generateID() int64 {
idCounter++
return idCounter
}