z5labs/gogm

Incorrect mapping parents/children nodes (same relation, same node) of cypher results into structures

lomoval opened this issue · 3 comments

Bug Report:

Mapping results into structures adds created structures from relations into main result slice (checked on SCHEMA_LOAD_STRATEGY only).
E.g.
we have such graph
image
and such structure

type Node struct {
		gogm.BaseNode
		Name     string  `gogm:"name=name"`
		Children []*Node `gogm:"direction=outgoing;relationship=HAS_NODE"`
		Parents  []*Node `gogm:"direction=incoming;relationship=HAS_NODE"`
	}

when loading 1 node by filter (name=n1) and depth=1 (LoadAllDepthFilter(context.Background(), &nodes, 1, filter, nil)) , you get all 3 nodes in the results - nodes slice (see full example in the end).

Expected Behavior

nodes slice contains 1 node with name=n1 and this node has other 2 nodes in Children and Parents slices.
nodes should not contain nodes with other name value.

Current Behavior

nodes slice contains 3 node.

Steps to Reproduce

  1. Create Graph structure in the Neo4j
create  (`14` :Node {name:'n1'}) ,
  (`15` :Node {name:'n2'}) ,
  (`16` :Node {name:'n3'}) ,
  (`14`)-[:`HAS_NODE` ]->(`15`),
  (`16`)-[:`HAS_NODE` ]->(`14`)
  1. Create a filter to get node with name=n1
filter := gocypherdsl.
		C(&gocypherdsl.ConditionConfig{Name: "n", Label: "Node"}).
		And(&gocypherdsl.ConditionConfig{
			Name:              "n",
			Field:             "name",
			ConditionOperator: gocypherdsl.EqualToOperator,
			Check:             "n1",
		})
  1. Use LoadAllDepthFilter(context.Background(), &nodes, 1, filter, nil) to load nodes.

Possible Solution

I think some problem with mapping Cypher results into structures.
Query works fine (has 1 node with two relations), but mapping of results adds all Node into main result slice.

MATCH (n:Node) WHERE n:Node AND n.name = 'n1' RETURN n , [[(n)-[r_H_1:HAS_NODE]->(n_N_1:Node) | [r_H_1, n_N_1]], [(n)<-[r_H_1:HAS_NODE]-(n_N_1:Node) | [r_H_1, n_N_1]]]

It is necessary to add structures from relations to the corresponding slices of the found structures, but not to the main results (If the structure does not match the filter).

Environment

Value
Go Version go1.18.3 windows/amd64
GoGM Version v2.3.6
Neo4J Version 4.4.3 community
Operating System Win 10

Would you be interested in tackling this issue

No

Additional info

Code example:

package main

import (
	"context"
	"fmt"
	gocypherdsl "github.com/mindstand/go-cypherdsl"
	"github.com/mindstand/gogm/v2"
	"github.com/rs/zerolog/log"
)

func testSameRef() {
	type Node struct {
		gogm.BaseNode
		Name     string  `gogm:"name=name"`
		Children []*Node `gogm:"direction=outgoing;relationship=HAS_NODE"`
		Parents  []*Node `gogm:"direction=incoming;relationship=HAS_NODE"`
	}

	g, err := gogm.New(
		&gogm.Config{
			Host:          "127.0.0.1",
			Port:          7688,
			Username:      "neo4j",
			Password:      "neo4j1",
			PoolSize:      50,
			Logger:        gogm.GetDefaultLogger(),
			LogLevel:      "DEBUG",
			IndexStrategy: gogm.IGNORE_INDEX,
			LoadStrategy:  gogm.SCHEMA_LOAD_STRATEGY,
		},
		gogm.DefaultPrimaryKeyStrategy,
		&Node{},
	)
	if err != nil {
		log.Err(err).Msg("")
		return
	}

	sess, err := g.NewSessionV2(gogm.SessionConfig{AccessMode: gogm.AccessModeRead})
	if err != nil {
		log.Err(err).Msg("")
		return
	}

	filter := gocypherdsl.
		C(&gocypherdsl.ConditionConfig{Name: "n", Label: "Node"}).
		And(&gocypherdsl.ConditionConfig{
			Name:              "n",
			Field:             "name",
			ConditionOperator: gocypherdsl.EqualToOperator,
			Check:             "n1",
		})
	if err != nil {
		log.Err(err).Msg("")
		return
	}

	var nodes []*Node
	err = sess.LoadAllDepthFilter(context.Background(), &nodes, 1, filter, nil)
	if err != nil {
		log.Err(err).Msg("")
		return
	}

	// Gives
	// 1 name: n1
	// 2 name: n2
	// 3 name: n3
	for i, node := range nodes {
		fmt.Printf("%d name: %s\n", i+1, node.Name)
	}
}

Hi @lomoval, thank you for the thorough bug report and apologies for the delayed reply. I'm going to reproduce and debug this issue with the examples you provided. I will get back to you here with updates. Expect to hear something in the next couple of days!

@erictg This worked fine for me with children of different type, but I got the same problem with Query method.
i.e. if you do smth like this:
user := new(model.User)
sess.Query(ctx, MATCH (u {user_id: $id})-[:OWNS]->(p:Product) RETURN u, COLLECT(p) as products, map[string]interface{}{"id": userID}, user)
user.Products are nil, though it returns products as separate nodes, just doesn't map them as relations.
While it's critical for me to have such behaviour, as I don't want to load all user's relations

@atymkiv
To map results of Cypher query into GOGM structures with relations you need to have info about relations in a response.
Collect only creates an array with nodes.
With SCHEMA_LOAD_STRATEGY you can try such query:

MATCH (u {user_id: $id}) RETURN u, [[(u)-[r:OWNS]->(p:Product) | [r, p]]]

In this case you will have u node and array with relation and nodes. Based on this info and internal config (struct fields - relation) GOGM will fill user.Products.
GOGM generates same queries but for all relations and for needed depth.