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