From c7c829c6bf441c1f20f09aff1e14faa1df1c1b1a Mon Sep 17 00:00:00 2001 From: Joris <joris.l@hotmail.com> Date: Thu, 25 Nov 2021 16:09:42 +0100 Subject: [PATCH] Converter now handles small cycles in the query Does need more testing though --- cypher/{healthChecks.go => clustering.go} | 0 cypher/convertQuery.go | 86 +++++++++- cypher/convertQuery_test.go | 197 ++++++++++++++++++++++ entity/queryStruct.go | 15 +- 4 files changed, 290 insertions(+), 8 deletions(-) rename cypher/{healthChecks.go => clustering.go} (100%) diff --git a/cypher/healthChecks.go b/cypher/clustering.go similarity index 100% rename from cypher/healthChecks.go rename to cypher/clustering.go diff --git a/cypher/convertQuery.go b/cypher/convertQuery.go index f15a3d3..6575fe7 100644 --- a/cypher/convertQuery.go +++ b/cypher/convertQuery.go @@ -42,7 +42,7 @@ func createCypher(JSONQuery *entity.IncomingQueryJSON) (*string, error) { // create the hierarchy from the cluster hierarchy, err := createQueryHierarchy(JSONQuery) if err != nil { - return nil, errors.New("Unable to create hierarchy in query, perhaps there is a dependency loop?") + return nil, err } // translate it to cypher in the right order, using the hierarchy @@ -143,6 +143,7 @@ func createQueryHierarchy(JSONQuery *entity.IncomingQueryJSON) (entity.Query, er // Add relations all to query parts log.Println(JSONQuery.Relations) for _, rel := range JSONQuery.Relations { + part := entity.QueryPart{ QType: "relation", QID: rel.ID, @@ -155,6 +156,35 @@ func createQueryHierarchy(JSONQuery *entity.IncomingQueryJSON) (entity.Query, er } + // // Here comes a checker for (A)-->(B) and (B)-->(A). This can be converted to one relation with the other as a constraint + // // Lets call it a small cycle. It wont catch bigger cycles (with 3 nodes for example) + + // // ** Note: nog code schrijven die hiervan een goeie exist call kan maken + // // EN de return statement is ook belangrijk + // for i := 0; i < len(parts); i++ { + // rel := JSONQuery.FindR(parts[i].QID) + // if rel.FromID == -1 || rel.ToID == -1 { + // continue + // } + + // for j := i + 1; j < len(parts); j++ { + // other := JSONQuery.FindR(parts[j].QID) + + // if other.FromID == -1 || other.ToID == -1 { + // continue + // } + + // if (rel.ToID == other.FromID && rel.FromID == other.ToID) { + // // Thus a cycle: Add the part as a nested one and remove it from the query + + // parts[i].NestedPart = &parts[j] + // parts = entity.Remove(parts, j) + // j-- + + // } + // } + // } + // Add the Groupby's for _, gb := range JSONQuery.GroupBys { part := entity.QueryPart{ @@ -266,6 +296,60 @@ func createQueryHierarchy(JSONQuery *entity.IncomingQueryJSON) (entity.Query, er } + // Here comes a checker for (A)-->(B) and (B)-->(A). This is mitigated partly by ignoring it + // (It appears the wrong is with neo4j in returning strange results, after querying the same query in 4 different ways) + // Lets call it a small cycle. It wont catch bigger cycles (with 3 nodes for example) + // It needs to create a new type of constraint (which createConstraints.go needs to be able to handle) + + // ** NOTE: dit is nogal even een open kwestie, want cypher geeft vage antwoorden op simpele queries met dit principe, + // er komen vage extra relations bij die niet aan de patronen voldoen, terwijl je zou zeggen dat het simpel moet zijn + // en cypher heeft geen INTERSECT maar wel een UNION #hoedan + + // Misschien werkt dit al, want dit werkt ook: + // Match (a:Person)-[r:ACTED_IN]-(m:Movie) + // with * + // Match (m:Movie)-[f:DIRECTED]-(a) + // return count (*) + + // ** Kijk of dit ook nog werkt als er andere relations afhangen van deze cycle!! + + for _, p := range parts { + // We only allow small cycles with relations + if p.QType != "relation" { + continue + } + + for _, dep := range p.Dependencies { + other := parts.SelectByID(dep) + + if other.QType != "relation" { + continue + } + + cycle := false + toRemove := -1 + + for i, otherDep := range other.Dependencies { + if otherDep == p.PartID { + // small cycle detected + + cycle = true + toRemove = i + } + } + + if cycle { + if len(other.Dependencies) == 0 { + other.Dependencies = make([]int, 0) + } else { + other.Dependencies[toRemove] = other.Dependencies[len(other.Dependencies)-1] + other.Dependencies = other.Dependencies[:len(other.Dependencies)-1] + } + + } + } + } + // Now we have a directed graph, meaning we can use some topological sort (Kahn's algorithm) var sortedQuery entity.Query incomingEdges := make(map[int]int) diff --git a/cypher/convertQuery_test.go b/cypher/convertQuery_test.go index fd325d2..636353e 100644 --- a/cypher/convertQuery_test.go +++ b/cypher/convertQuery_test.go @@ -814,3 +814,200 @@ func Test6(t *testing.T) { fmt.Println(*cypher) t.Fail() } +func TestNoRelation(t *testing.T) { + // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D + query := []byte(`{ + "return": { + "entities": [ + 11, + 12 + ], + "relations": [ + 10 + ], + "groupBys": [] + }, + "entities": [ + { + "name": "Person", + "ID": 11, + "constraints": [] + }, + { + "name": "Movie", + "ID": 12, + "constraints": [] + } + ], + "relations": [], + "groupBys": [], + "machineLearning": [], + "limit": 5000, + "databaseName": "Movies3" + } + `) + + var JSONQuery entity.IncomingQueryJSON + json.Unmarshal(query, &JSONQuery) + fmt.Println(JSONQuery) + fmt.Println(" ") + + s := NewService() + _, err := s.ConvertQuery(&JSONQuery) + if err != nil { + assert.Equal(t, err.Error(), "Invalid query") + } else { + // It should error, thus it must not reach this + t.Fail() + } +} + +func TestNoEntities(t *testing.T) { + // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D + query := []byte(`{ + "return": { + "entities": [ + 11, + 12 + ], + "relations": [ + 10 + ], + "groupBys": [] + }, + "entities": [], + "relations": [ + { + "ID": 10, + "name": "DIRECTED", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 11, + "toType": "entity", + "toID": 12, + "constraints": [] + } + ], + "groupBys": [], + "machineLearning": [], + "limit": 5000, + "databaseName": "Movies3" + } + `) + + var JSONQuery entity.IncomingQueryJSON + json.Unmarshal(query, &JSONQuery) + fmt.Println(JSONQuery) + fmt.Println(" ") + + s := NewService() + _, err := s.ConvertQuery(&JSONQuery) + if err != nil { + assert.Equal(t, err.Error(), "Invalid query") + } else { + // It should error, thus it must not reach this + t.Fail() + } +} +func TestTwoRelations(t *testing.T) { + // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D + query := []byte(`{ + "return": { + "entities": [ + 11, + 12 + ], + "relations": [ + 10 + ], + "groupBys": [] + }, + "entities": [ + { + "name": "Person", + "ID": 11, + "constraints": [] + }, + { + "name": "Movie", + "ID": 12, + "constraints": [] + } + ], + "relations": [ + { + "ID": 10, + "name": "DIRECTED", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 11, + "toType": "entity", + "toID": 12, + "constraints": [] + }, + { + "ID": 11, + "name": "ACTED_IN", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 12, + "toType": "entity", + "toID": 11, + "constraints": [] + } + ], + "groupBys": [], + "machineLearning": [], + "limit": 5000, + "databaseName": "Movies3" + } + `) + + var JSONQuery entity.IncomingQueryJSON + json.Unmarshal(query, &JSONQuery) + fmt.Println(JSONQuery) + fmt.Println(" ") + + s := NewService() + cypher, err := s.ConvertQuery(&JSONQuery) + if err != nil { + fmt.Println(err) + t.Fail() + return + } + + if cypher == nil { + t.Fail() + return + } + fmt.Println(*cypher) + t.Fail() + // fmt.Println(*cypher) + + // answer := `MATCH p0 = (e11:Person)-[:DIRECTED*1..1]-(e12:Movie) + // UNWIND relationships(p0) as r10 + // WITH * + // MATCH p1 = (e11:Person)-[:DIRECTED*1..1]-(e12:Movie) + // UNWIND relationships(p1) as r10 + // WITH * + // RETURN r10, e11, e12, r10, e11, e12 + // LIMIT 5000;nodelink` + + // trimmedCypher := strings.Replace(*cypher, "\n", "", -1) + // trimmedCypher = strings.Replace(trimmedCypher, "\t", "", -1) + + // trimmedAnswer := strings.Replace(answer, "\n", "", -1) + // trimmedAnswer = strings.Replace(trimmedAnswer, "\t", "", -1) + + // fmt.Println(*cypher) + // assert.Equal(t, trimmedAnswer, trimmedAnswer) +} diff --git a/entity/queryStruct.go b/entity/queryStruct.go index 66b99a3..fe43b58 100644 --- a/entity/queryStruct.go +++ b/entity/queryStruct.go @@ -117,10 +117,11 @@ func (JSONQuery IncomingQueryJSON) FindG(qID int) *QueryGroupByStruct { } type QueryPart struct { - QType string // Eg if it is a relation or groupby - QID int // ID of said relation/gb - PartID int // Custom ID used for dependency - Dependencies []int // List of partID's that need to come before + QType string // Eg if it is a relation or groupby + QID int // ID of said relation/gb + PartID int // Custom ID used for dependency + Dependencies []int // List of partID's that need to come before + NestedPart *QueryPart // Pointer to another part, used in some cases to avoid cycles } type Query []QueryPart @@ -135,9 +136,9 @@ func (q Query) Find(qID int, qType string) *QueryPart { } func (q Query) SelectByID(ID int) *QueryPart { - for _, part := range q { - if part.PartID == ID { - return &part + for i := range q { + if q[i].PartID == ID { + return &q[i] } } return nil -- GitLab