From c4fd8401554d2ec7b5c957b89201e4fabb027d9c Mon Sep 17 00:00:00 2001
From: Fjodor <fjodor_rs@hotmail.com>
Date: Thu, 27 May 2021 16:01:41 +0200
Subject: [PATCH] Added relations, relations constraints, removed modifiers
 @Lorenzo

---
 cypher/convertQuery.go      | 273 ++++++++++++++----------------------
 cypher/createConstraints.go |  13 +-
 main/main.go                |  63 ++++++++-
 3 files changed, 167 insertions(+), 182 deletions(-)

diff --git a/cypher/convertQuery.go b/cypher/convertQuery.go
index fd7d8de..a0c998c 100644
--- a/cypher/convertQuery.go
+++ b/cypher/convertQuery.go
@@ -3,6 +3,7 @@ package cypher
 import (
 	"errors"
 	"fmt"
+	"strings"
 
 	"git.science.uu.nl/datastrophe/query-conversion/entity"
 )
@@ -46,6 +47,23 @@ func (s *Service) ConvertQuery(JSONQuery *entity.IncomingQueryJSON) (*string, er
 	return result, nil
 }
 
+func sliceContains(s []int, e int) bool {
+	for _, a := range s {
+		if a == e {
+			return true
+		}
+	}
+	return false
+}
+
+/*TrimSuffix trims the final character of a string */
+func TrimSuffix(s, suffix string) string {
+	if strings.HasSuffix(s, suffix) {
+		s = s[:len(s)-len(suffix)]
+	}
+	return s
+}
+
 /* createQuery generates a query based on the json file provided
 Parameters: jsonQuery is a parsedJSON struct holding all the data needed to form a query
 
@@ -64,42 +82,84 @@ func createQuery(JSONQuery *entity.IncomingQueryJSON) *string {
 		nodesToReturn     []string
 		nodeUnion         string
 		relationUnion     string
+		queryList         [][][]int
+		entityList        []int
+		ret               string
 	)
 
-	// Loop over all relations
-	ret := ""
-
 	for i, relation := range JSONQuery.Relations {
+		var contains bool
+		contains = false
+		for j := range queryList {
+			if sliceContains(queryList[j][0], relation.EntityFrom) || sliceContains(queryList[j][0], relation.EntityTo) {
+				if !sliceContains(queryList[j][0], relation.EntityFrom) {
+					queryList[j][0] = append(queryList[j][0], relation.EntityFrom)
+					entityList = append(entityList, relation.EntityFrom)
+				}
+				if !sliceContains(queryList[j][0], relation.EntityTo) {
+					queryList[j][0] = append(queryList[j][0], relation.EntityTo)
+					entityList = append(entityList, relation.EntityTo)
+				}
+				queryList[j][1] = append(queryList[j][1], i)
+				contains = true
+			}
+		}
+		if !contains {
+			queryList = append(queryList, [][]int{{relation.EntityFrom, relation.EntityTo}, {i}})
+		}
+	}
 
-		relationName := fmt.Sprintf("r%v", i)
-
-		if relation.EntityFrom >= 0 {
-			// if there is a from-node
-			// create the let for this node
-			fromName := fmt.Sprintf("n%v", relation.EntityFrom)
+	for i := range queryList {
+		//reset variables for the next query
+		nodeUnion = ""
+		relationUnion = ""
+		relationsToReturn = []string{}
+		for j, relationID := range queryList[i][1] {
+			relationName := fmt.Sprintf("r%v", j)
+			relation := JSONQuery.Relations[relationID]
+			pathName := fmt.Sprintf("p%v", j)
+			relationsToReturn = append(relationsToReturn, pathName)
+			if relation.EntityFrom >= 0 {
+				// if there is a from-node
+				// create the let for this node
+				fromName := fmt.Sprintf("n%v", relation.EntityFrom)
+
+				ret += *createNodeMatch(&JSONQuery.Entities[relation.EntityFrom], &fromName)
+
+				ret += *createRelationMatch(&relation, relationName, pathName, &JSONQuery.Entities, JSONQuery.Limit, true)
+			} else if relation.EntityTo >= 0 {
+				// if there is only a to-node
+				toName := fmt.Sprintf("n%v", relation.EntityTo)
+
+				ret += *createNodeMatch(&JSONQuery.Entities[relation.EntityTo], &toName)
+
+				ret += *createRelationMatch(&relation, relationName, pathName, &JSONQuery.Entities, JSONQuery.Limit, false)
+				// Add this relation to the list
+			} else {
+				fmt.Println("Relation-only queries are currently not supported")
+				continue
+			}
+		}
 
-			ret += *createNodeLet(&JSONQuery.Entities[relation.EntityFrom], &fromName)
+		// Create UNION statements that create unique lists of all the nodes and relations
 
-			ret += *createRelationLetWithFromEntity(&relation, relationName, &JSONQuery.Entities, JSONQuery.Limit)
-		} else if relation.EntityTo >= 0 {
-			// if there is only a to-node
-			toName := fmt.Sprintf("n%v", relation.EntityTo)
+		// Thus removing all duplicates
+		nodeUnion = "RETURN "
 
-			ret += *createNodeLet(&JSONQuery.Entities[relation.EntityTo], &toName)
+		for _, entityID := range queryList[i][0] {
+			if sliceContains(JSONQuery.Return.Entities, entityID) {
+				nodeUnion += fmt.Sprintf("n%v,", entityID)
+			}
+		}
 
-			ret += *createRelationLetWithOnlyToEntity(&relation, relationName, &JSONQuery.Entities, JSONQuery.Limit)
-			// Add this relation to the list
-		} else {
-			fmt.Println("Relation-only queries are currently not supported")
-			continue
+		for _, relation := range relationsToReturn {
+			relationUnion += fmt.Sprintf("%v,", relation)
 		}
 
-		// Add this relation to the list
-		relationsToReturn = append(relationsToReturn, relationName)
+		relationUnion = TrimSuffix(relationUnion, ",")
+		ret += nodeUnion + relationUnion + ";\n"
 	}
 
-	// Add node let statements for nodes that are not yet returned
-	// Create a set from all the entity-from's and entity-to's, to check if they are returned
 	nodeSet := make(map[int]bool)
 	for _, relation := range JSONQuery.Relations {
 		nodeSet[relation.EntityFrom] = true
@@ -111,103 +171,13 @@ func createQuery(JSONQuery *entity.IncomingQueryJSON) *string {
 		if !nodeSet[entityIndex] {
 			// If not, return this node
 			name := fmt.Sprintf("n%v", entityIndex)
-			ret += *createNodeLet(&JSONQuery.Entities[entityIndex], &name)
+			ret += *createNodeMatch(&JSONQuery.Entities[entityIndex], &name)
 
 			// Add this node to the list
 			nodesToReturn = append(nodesToReturn, name)
 		}
 	}
 
-	//If there are modifiers within the query, we run a different set of checks which focus on quantifiable aspects
-	if len(JSONQuery.Modifiers) > 0 {
-		modifier := JSONQuery.Modifiers[0]
-		// There is a distinction between (relations and entities) and (relations or entities)
-		if len(JSONQuery.Return.Relations) > 0 && len(JSONQuery.Return.Entities) > 0 {
-
-			var pathDistinction string // .vertices or .edges
-
-			// Select the correct addition to the return of r0[**]
-			if modifier.SelectedType == "entity" {
-				// ASSUMING THERE IS ONLY 1 RELATION
-				if JSONQuery.Relations[0].EntityFrom == modifier.ID {
-					pathDistinction = fmt.Sprintf(".vertices[%v]", JSONQuery.Relations[0].Depth.Min-1)
-
-				} else {
-					pathDistinction = fmt.Sprintf(".vertices[%v]", JSONQuery.Relations[0].Depth.Max)
-
-				}
-			} else {
-				pathDistinction = ".edges[**]"
-			}
-
-			// Getting the attribute if there is one
-			if modifier.AttributeIndex != -1 {
-				if modifier.SelectedType == "entity" {
-					pathDistinction += fmt.Sprintf(".%v", JSONQuery.Entities[modifier.ID].Constraints[modifier.AttributeIndex].Attribute)
-
-				} else {
-					pathDistinction += fmt.Sprintf(".%v", JSONQuery.Relations[modifier.ID].Constraints[modifier.AttributeIndex].Attribute)
-
-				}
-			}
-
-			// If count is used it has to be replaced with Length + unique else use the modifier type
-			if modifier.Type == "COUNT" {
-				ret += fmt.Sprintf("RETURN LENGTH (unique(r0[*]%v))", pathDistinction)
-
-			} else {
-				ret += fmt.Sprintf("RETURN %v (r0[*]%v)", modifier.Type, pathDistinction)
-
-			}
-
-		} else {
-			// Check if the modifier is on an attribute
-			if modifier.AttributeIndex == -1 {
-				ret += fmt.Sprintf("RETURN LENGTH (n%v)", modifier.ID)
-			} else {
-				var attribute string
-
-				// Selecting the right attribute from either the entity constraint or relation constraint
-				if modifier.SelectedType == "entity" {
-					attribute = JSONQuery.Entities[modifier.ID].Constraints[modifier.AttributeIndex].Attribute
-
-				} else {
-					attribute = JSONQuery.Relations[modifier.ID].Constraints[modifier.AttributeIndex].Attribute
-
-				}
-
-				// If count is used it has to be replaced with Length + unique else use the modifier type
-				if modifier.Type == "COUNT" {
-					ret += fmt.Sprintf("RETURN LENGTH (unique(n%v[*].%v))", modifier.ID, attribute)
-
-				} else {
-					ret += fmt.Sprintf("RETURN %v (n%v[*].%v)", modifier.Type, modifier.ID, attribute)
-
-				}
-			}
-		}
-
-	} else {
-
-		// Create UNION statements that create unique lists of all the nodes and relations
-		// Thus removing all duplicates
-		nodeUnion = "\nRETURN "
-
-		for _, node := range nodesToReturn {
-			nodeUnion += fmt.Sprintf("%v,", node)
-		}
-		// RETURN n0, n1, n2, nn, r0, r1, r2, r3, rn
-		relationUnion = "LET edges = first(RETURN UNION_DISTINCT("
-		for _, relation := range relationsToReturn {
-			relationUnion += fmt.Sprintf("flatten(%v[**].edges), ", relation)
-		}
-		relationUnion += "[],[]))\n"
-
-		ret += nodeUnion + relationUnion
-		ret += "RETURN {\"vertices\":nodes, \"edges\":edges }"
-
-	}
-
 	return &ret
 }
 
@@ -217,9 +187,9 @@ name is the autogenerated name of the node consisting of "n" + the index of the
 
 Return: a string containing a single LET-statement in AQL
 */
-func createNodeLet(node *entity.QueryEntityStruct, name *string) *string {
-	header := fmt.Sprintf("MATCH (%v : %v)\n", *name, node.Type)
-	constraints := *createConstraintStatements(&node.Constraints, *name, false)
+func createNodeMatch(node *entity.QueryEntityStruct, name *string) *string {
+	header := fmt.Sprintf("MATCH (%v:%v)\n", *name, node.Type)
+	constraints := *createConstraintStatements(&node.Constraints, *name)
 	ret := header + constraints
 	return &ret
 }
@@ -231,63 +201,26 @@ entities is a list of entityStructs that are needed to form the relation LET-sta
 
 Return: a string containing a single LET-statement in AQL
 */
-func createRelationLetWithFromEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int) *string {
-	header := fmt.Sprintf("LET %v = (\n\tFOR x IN n%v \n", name, relation.EntityFrom)
-	forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v OUTBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Type)
-
-	// Guarantees that there is no path returned with a duplicate edge
-	// This way there are no cycle paths possible, TODO: more research about this needed
-	optionStmtn := "\tOPTIONS { uniqueEdges: \"path\" }\n"
-
-	vFilterStmnt := ""
-	if relation.EntityTo != -1 {
-		// If there is a to-node, generate the filter statement
-		toConstraints := (*entities)[relation.EntityTo].Constraints
-		vFilterStmnt += *createConstraintStatements(&toConstraints, "v", false)
-
-		// Add a WITH statement if the collection of entityTo is not yet included
-		if (*entities)[(*relation).EntityFrom].Type != (*entities)[(*relation).EntityTo].Type {
-			header = fmt.Sprintf("WITH %v\n %v", (*entities)[(*relation).EntityTo].Type, header)
-		}
-	}
-
-	relationFilterStmnt := *createConstraintStatements(&relation.Constraints, "p", true)
+func createRelationMatch(relation *entity.QueryRelationStruct, relationName string, pathName string, entities *[]entity.QueryEntityStruct, limit int, outbound bool) *string {
+	relationReturn := ""
+	var relationBounds int
+	if outbound {
+		relationReturn = fmt.Sprintf("MATCH %v = (n%v)-[%v:%v*%v..%v]->(", pathName, relation.EntityFrom, relationName, relation.Type, relation.Depth.Min, relation.Depth.Max)
+		relationBounds = relation.EntityTo
 
-	// Dont use a limit on quantifing queries
-	footer := ""
-	if limit != -1 {
-		footer += fmt.Sprintf("\tLIMIT %v \n", limit)
+	} else {
+		relationReturn = fmt.Sprintf("MATCH %v = (n%v)-[%v:%v*%v..%v]->(", pathName, relation.EntityTo, relationName, relation.Type, relation.Depth.Min, relation.Depth.Max)
+		relationBounds = relation.EntityFrom
 	}
-	footer += "RETURN DISTINCT p )\n"
-
-	ret := header + forStatement + optionStmtn + vFilterStmnt + relationFilterStmnt + footer
-	return &ret
-}
-
-/* createRelationLetWithOnlyToEntity generates a 'LET' statement for relations with only an 'EntityTo' property
-Parameters: relation is a relation struct containing the information of a single relation,
-name is the autogenerated name of the node consisting of "r" + the index of the relation,
-entities is a list of entityStructs that are needed to form the relation LET-statement
 
-Return: a string containing a single LET-statement in AQL
-*/
-func createRelationLetWithOnlyToEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int) *string {
-	header := fmt.Sprintf("LET %v = (\n\tFOR x IN n%v \n", name, relation.EntityTo)
-	forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v INBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Type)
-
-	// Guarantees that there is no path returned with a duplicate edge
-	// This way there are no cycle paths possible, TODO: more research about this needed
-	optionStmtn := "\tOPTIONS { uniqueEdges: \"path\" }\n"
+	if relationBounds != -1 {
+		relationReturn += fmt.Sprintf("n%v", relationBounds)
+	}
+	relationReturn += ")"
 
-	relationFilterStmnt := *createConstraintStatements(&relation.Constraints, "p", true)
+	constraintReturn := *createConstraintStatements(&relation.Constraints, relationName)
 
-	// Dont use a limit on quantifing queries
-	footer := ""
-	if limit != -1 {
-		footer += fmt.Sprintf("\tLIMIT %v \n", limit)
-	}
-	footer += "RETURN DISTINCT p )\n"
+	ret := relationReturn + "\n" + constraintReturn
 
-	ret := header + forStatement + optionStmtn + relationFilterStmnt + footer
 	return &ret
 }
diff --git a/cypher/createConstraints.go b/cypher/createConstraints.go
index 7bd0627..e44b42e 100644
--- a/cypher/createConstraints.go
+++ b/cypher/createConstraints.go
@@ -13,7 +13,7 @@ isRelation is a boolean specifying if this constraint comes from a node or relat
 
 Return: a string containing a FILTER-statement with all the constraints
 */
-func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, name string, isRelation bool) *string {
+func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, name string) *string {
 	s := ""
 	if len(*constraints) == 0 {
 		return &s
@@ -22,7 +22,7 @@ func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, nam
 	newLineStatement := "\tWHERE"
 
 	for _, v := range *constraints {
-		s += fmt.Sprintf("%v %v \n", newLineStatement, *createConstraintBoolExpression(&v, name, isRelation))
+		s += fmt.Sprintf("%v%v \n", newLineStatement, *createConstraintBoolExpression(&v, name))
 		newLineStatement = "\tAND"
 	}
 
@@ -38,7 +38,7 @@ isRelation is a boolean specifying if this constraint comes from a node or relat
 
 Return: a string containing an boolean expression of a single constraint
 */
-func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, name string, isRelation bool) *string {
+func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, name string) *string {
 	var (
 		match string
 		value string
@@ -95,10 +95,7 @@ func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, na
 		}
 	}
 
-	if isRelation {
-		line = fmt.Sprintf("%s.edges[*].%s ALL %s %s", name, constraint.Attribute, match, value)
-	} else {
-		line = fmt.Sprintf("%s %s.%s %s %s", neq, name, constraint.Attribute, match, value)
-	}
+	line = fmt.Sprintf("%s %s.%s %s %s", neq, name, constraint.Attribute, match, value)
+
 	return &line
 }
diff --git a/main/main.go b/main/main.go
index 38dbc26..1d4b065 100644
--- a/main/main.go
+++ b/main/main.go
@@ -14,11 +14,38 @@ func main() {
 	js := []byte(`{
 		"return": {
 			"entities": [
-				0 
+				0,
+				1,
+				2
 			],
-			"relations": []
+			"relations": [
+				0,
+				1
+			]
 		},
 		"entities": [
+			{
+				"type": "airports",
+				"constraints": [
+					{
+						"attribute": "city",
+						"value": "New York",
+						"dataType": "text",
+						"matchType": "exact"
+					}
+				]
+			},
+			{
+				"type": "airports",
+				"constraints": [
+					{
+						"attribute": "city",
+						"value": "San Francisco",
+						"dataType": "text",
+						"matchType": "exact"
+					}
+				]
+			},
 			{
 				"type": "airports",
 				"constraints": [
@@ -31,9 +58,37 @@ func main() {
 				]
 			}
 		],
-		"relations": [],
+		"relations": [
+			{
+				"type": "flights",
+				"depth": {
+					"min": 1,
+					"max": 3
+				},
+				"entityFrom": 2,
+				"entityTo": 1,
+				"constraints": [
+					{
+						"attribute": "Day",
+						"value": "15",
+						"dataType": "number",
+						"matchType": "EQ"
+					}
+				]
+			},
+			{
+				"type": "flights",
+				"depth": {
+					"min": 1,
+					"max": 1
+				},
+				"entityFrom": 0,
+				"entityTo": -1,
+				"constraints": []
+			}
+		],
 		"limit": 5000
-		}`)
+	}`)
 
 	var inc entity.IncomingQueryJSON
 	json.Unmarshal(js, &inc)
-- 
GitLab