/* This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course. © Copyright Utrecht University (Department of Information and Computing Sciences) */ package cypher import ( "errors" "fmt" "strings" "git.science.uu.nl/graphpolaris/query-conversion/entity/entitycypher" ) /* ConvertQuery converts an IncomingQueryJSON object into AQL JSONQuery: *entity.IncomingQueryJSON, the query to be converted to AQL Returns: (*string, error), the AQL query and a possible error */ func (s *Service) ConvertQuery(JSONQuery *entitycypher.IncomingQueryJSON) (*string, error) { // Check to make sure all indexes exist // How many entities are there numEntities := len(JSONQuery.Entities) - 1 // How many relations there are numRelations := len(JSONQuery.Relations) - 1 // Make sure no entity should be returned that is outside the range of that list for _, e := range JSONQuery.Return.Entities { // If this entity references an entity that is outside the range if e > numEntities || e < 0 { return nil, errors.New("non-existing entity referenced in return") } } // Make sure that no relation mentions a non-existing entity for _, r := range JSONQuery.Relations { if r.EntityFrom > numEntities || r.EntityTo > numEntities { return nil, errors.New("non-exisiting entity referenced in relation") } } // Make sure no non-existing relation is tried to be returned for _, r := range JSONQuery.Return.Relations { if r > numRelations || r < 0 { return nil, errors.New("non-existing relation referenced in return") } } result := createQuery(JSONQuery) return result, nil } /* sliceContains checks if a slice contains the input s: []int, the slice to check e: int, what you're checking for Return: bool, true if it contains 'e' */ 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 JSONQuery: *entity.IncomingQueryJSON, jsonQuery is a parsedJSON struct holding all the data needed to form a query Return: *string, a string containing the corresponding AQL query and an error */ func createQuery(JSONQuery *entitycypher.IncomingQueryJSON) *string { // Note: Case #4, where there is an edge only query (without any entity), is not supported by frontend // If a modifier is used, disable the limit if len(JSONQuery.Modifiers) > 0 { JSONQuery.Limit = -1 } var ( relationsToReturn []string nodesToReturn []string nodeUnion string relationUnion string queryList [][][]int entityList []int ret string ) 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}}) } } 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 } } // Create UNION statements that create unique lists of all the nodes and relations // Thus removing all duplicates nodeUnion = "RETURN " for _, entityID := range queryList[i][0] { if sliceContains(JSONQuery.Return.Entities, entityID) { nodeUnion += fmt.Sprintf("n%v,", entityID) } } for _, relation := range relationsToReturn { relationUnion += fmt.Sprintf("%v,", relation) } relationUnion = TrimSuffix(relationUnion, ",") // hier zat een newline ret += nodeUnion + relationUnion + "; " } nodeSet := make(map[int]bool) for _, relation := range JSONQuery.Relations { nodeSet[relation.EntityFrom] = true nodeSet[relation.EntityTo] = true } // Check if the entities to return are already returned for _, entityIndex := range JSONQuery.Return.Entities { if !nodeSet[entityIndex] { // If not, return this node name := fmt.Sprintf("n%v", entityIndex) ret += *createNodeMatch(&JSONQuery.Entities[entityIndex], &name) // Add this node to the list nodesToReturn = append(nodesToReturn, name) ret += fmt.Sprintf("RETURN %v", name) } } ret = TrimSuffix(ret, " ") return &ret } /* createNodeLet generates a 'LET' statement for a node related query node: *entity.QueryEntityStruct, node is an entityStruct containing the information of a single node, name: *string, is the autogenerated name of the node consisting of "n" + the index of the node Return: *string, a string containing a single LET-statement in AQL */ func createNodeMatch(node *entitycypher.QueryEntityStruct, name *string) *string { // hier zat een newline header := fmt.Sprintf("MATCH (%v:%v) ", *name, node.Type) constraints := *createConstraintStatements(&node.Constraints, *name) ret := header + constraints return &ret } /* createRelationLetWithFromEntity generates a 'LET' statement for relations with an 'EntityFrom' property and optionally an 'EntitiyTo' property relation: *entity.QueryRelationStruct, relation is a relation struct containing the information of a single relation, relationName: string, is the name of the relation, is the autogenerated name of the node consisting of "r" + the index of the relation, pathName: string, is the path of the name, entities: *[]entity.QueryEntityStruct, is a list of entityStructs that are needed to form the relation LET-statement limit: int, the limit for the number of nodes to return outbound: bool, checks if the relation is inbound or outbound Return: *string, a string containing a single LET-statement in AQL */ func createRelationMatch(relation *entitycypher.QueryRelationStruct, relationName string, pathName string, entities *[]entitycypher.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 } 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 } if relationBounds != -1 { relationReturn += fmt.Sprintf("n%v", relationBounds) } relationReturn += ")" constraintReturn := *createConstraintStatements(&relation.Constraints, relationName) // hier zat een newline ret := relationReturn + " " + constraintReturn return &ret }