diff --git a/cypher/convertQuery.go b/cypher/convertQuery.go index fd7d8de58c78481dc4c1a1a4ae15319859e1a043..a0c998c44b173f62064c5dfa682c3529b12378e9 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 7bd06270da18c5ef96e1fee6a9cbab7150f7e9c5..e44b42e4602e8bedcf302ce5dc5248737ca57a89 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 38dbc2647bb8e437502a31e43c019c5680863914..1d4b065fdfb1b8602bb179896e98ad07067c3290 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)