/* 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 aql import ( "errors" "fmt" "strconv" "git.science.uu.nl/graphpolaris/query-conversion/entity" ) // Version 1.13 /* ConvertQuery converts an IncomingQueryJSON object into AQL JSONQuery: *entity.IncomingQueryJSON, the query to be converted to AQL Returns: *string, the AQL query and a possible error */ func (s *Service) ConvertQuery(JSONQuery *entity.IncomingQueryJSON) (*string, error) { // Check to make sure all indexes exist // The largest possible id for an entity largestEntityID := len(JSONQuery.Entities) - 1 // The largest possible id for a relation largestRelationID := 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 > largestEntityID || 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 > largestEntityID || r.EntityTo > largestEntityID { 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 > largestRelationID || r < 0 { return nil, errors.New("non-existing relation referenced in return") } } result := createQuery(JSONQuery) return result, nil } /* createQuery generates a query based on the json file provided JSONQuery: *entity.IncomingQueryJSON, this 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 *entity.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 ) // If we've already used an entity we can set the value to true so we skip it in the result later entityDone := make(map[int]bool) for o := range JSONQuery.Entities { entityDone[o] = false } // Loop over all relations ret := "" // Add a WITH statement for entityTo includedTypes := make(map[string]bool) allTypes := make(map[string]bool) for _, relation := range JSONQuery.Relations { if relation.EntityFrom >= 0 { includedTypes[JSONQuery.Entities[relation.EntityFrom].Type] = true allTypes[JSONQuery.Entities[relation.EntityFrom].Type] = true // If the type is in the entityTo it is a valid type but not yet included if relation.EntityTo >= 0 { allTypes[JSONQuery.Entities[relation.EntityTo].Type] = true } } if relation.EntityFrom == -1 && relation.EntityTo >= 0 { includedTypes[JSONQuery.Entities[relation.EntityTo].Type] = true allTypes[JSONQuery.Entities[relation.EntityTo].Type] = true } } // Include all types that are not yet included first := true for k := range allTypes { if !includedTypes[k] { if first { ret += fmt.Sprintf("WITH %v", k) first = false } else { ret += fmt.Sprintf(", %v", k) } } } if !first { ret += "\n" } for i, relation := range JSONQuery.Relations { relationName := fmt.Sprintf("r%v", i) if relation.EntityFrom >= 0 { // if there is a from-node // create the let for this node // IF WE'VE ALREADY SEEN THIS ENTITY WE DON'T HAVE TO REQUERY IT, WE CAN JUST REUSE THE LET BINDING if !entityDone[relation.EntityFrom] { fromName := fmt.Sprintf("n%v", relation.EntityFrom) ret += *createNodeLet(&JSONQuery.Entities[relation.EntityFrom], &fromName) entityDone[relation.EntityFrom] = true } var function *entity.QueryFunctionStruct for _, f := range JSONQuery.Functions { if (f.GroupID == relation.EntityFrom && f.ByID == relation.EntityTo) || (f.GroupID == relation.EntityTo && f.ByID == relation.EntityFrom) { function = &f } } ret += *createRelationLetWithFromEntity(&relation, relationName, &JSONQuery.Entities, JSONQuery.Limit, function) } else if relation.EntityTo >= 0 { fmt.Println("Joris are you a madman! How did this happen?") // if there is only a to-node if !entityDone[relation.EntityTo] { toName := fmt.Sprintf("n%v", relation.EntityTo) ret += *createNodeLet(&JSONQuery.Entities[relation.EntityTo], &toName) entityDone[relation.EntityTo] = true } 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 } // Add this relation to the list relationsToReturn = append(relationsToReturn, relationName) } // 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 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 += *createNodeLet(&JSONQuery.Entities[entityIndex], &name) // Add this node to the list nodesToReturn = append(nodesToReturn, name) } } if len(JSONQuery.Functions) == 0 { //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.SelectedTypeID { // This should always be 0, because that is the start of the path pathDistinction = ".vertices[0]" } else { // Otherwise take the depth.max -1 to get the last 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.SelectedTypeID].Constraints[modifier.AttributeIndex].Attribute) } else { pathDistinction += fmt.Sprintf(".%v", JSONQuery.Relations[modifier.SelectedTypeID].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.SelectedTypeID) } else { var attribute string // Selecting the right attribute from either the entity constraint or relation constraint if modifier.SelectedType == "entity" { attribute = JSONQuery.Entities[modifier.SelectedTypeID].Constraints[modifier.AttributeIndex].Attribute } else { attribute = JSONQuery.Relations[modifier.SelectedTypeID].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.SelectedTypeID, attribute) } else { ret += fmt.Sprintf("RETURN %v (n%v[*].%v)", modifier.Type, modifier.SelectedTypeID, attribute) } } } } else { // Create UNION statements that create unique lists of all the nodes and relations // Thus removing all duplicates nodeUnion = "\nLET nodes = first(RETURN UNION_DISTINCT(" for _, relation := range relationsToReturn { nodeUnion += fmt.Sprintf("flatten(%v[**].vertices), ", relation) } for _, node := range nodesToReturn { nodeUnion += fmt.Sprintf("%v,", node) } nodeUnion += "[],[]))\n" 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 }" } } else { ret += createTableWithFunctions(JSONQuery.Functions, JSONQuery.Relations, JSONQuery.Entities) } 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 nod, 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 createNodeLet(node *entity.QueryEntityStruct, name *string) *string { header := fmt.Sprintf("LET %v = (\n\tFOR x IN %v \n", *name, node.Type) footer := "\tRETURN x\n)\n" constraints := *createConstraintStatements(&node.Constraints, "x", false) ret := header + constraints + footer return &ret } /* createRelationLetWithFromEntity generates a 'LET' statement for relations with an 'EntityFrom' property and optionally an 'EntitiyTo' property relation: *entity.QueryRekationStruct, relation is a relation struct containing the information of a single relation, name: string, is the autogenerated name of the node consisting of "r" + the index of the relation, entities: *[]entity.QueryEntityStrucy, is a list of entityStructs that are needed to form the relation LET-statement, Return: *string, a string containing a single LET-statement in AQL */ func createRelationLetWithFromEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int, function *entity.QueryFunctionStruct) *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) } relationFilterStmnt := *createConstraintStatements(&relation.Constraints, "p", true) // Dont use a limit on quantifing queries footer := "" if limit != -1 { footer += fmt.Sprintf("\tLIMIT %v \n", limit) } if function != nil { footer += "RETURN DISTINCT v )\n" } else { 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 relation: *entity.QueryRelationStruct, relation is a relation struct containing the information of a single relation, name: string, is the autogenerated name of the node consisting of "r" + the index of the relation, entities: *[]entity.QueryEntityStruct, is a list of entityStructs that are needed to form the relation LET-statement, Return: *string, 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" relationFilterStmnt := *createConstraintStatements(&relation.Constraints, "p", true) // Dont use a limit on quantifing queries footer := "" if limit != -1 { footer += fmt.Sprintf("\tLIMIT %v \n", limit) } footer += "RETURN DISTINCT p )\n" ret := header + forStatement + optionStmtn + relationFilterStmnt + footer return &ret } type variableNameGeneratorToken struct { token int } func newVariableNameGeneratorToken() *variableNameGeneratorToken { v := variableNameGeneratorToken{token: 0} return &v } func variableNameGenerator(vngt *variableNameGeneratorToken) string { result := "variable_" + strconv.Itoa(vngt.token) vngt.token++ return result } func createTableWithFunctions(functions []entity.QueryFunctionStruct, relations []entity.QueryRelationStruct, entities []entity.QueryEntityStruct) string { result := "" v := newVariableNameGeneratorToken() for _, function := range functions { for j, relation := range relations { if (function.GroupID == relation.EntityFrom && function.ByID == relation.EntityTo) || (function.GroupID == relation.EntityTo && function.ByID == relation.EntityFrom) { if function.Type == "groupBy" { a := variableNameGenerator(v) b := variableNameGenerator(v) c := variableNameGenerator(v) d := variableNameGenerator(v) e := variableNameGenerator(v) f := variableNameGenerator(v) g := variableNameGenerator(v) h := variableNameGenerator(v) rName := fmt.Sprintf("r%v", j) nName := fmt.Sprintf("n%v", relation.EntityFrom) result += "LET " + a + " = (\n\tFOR r IN " + relation.Type + "\n\tLET " + b + " = (\n\t\tFOR c IN " + rName + " \n\t\tFILTER c._id == r._to \n\t\tRETURN c." + function.GroupAttribute + "\n\t) " + "\n\tLET " + c + " = " + b + "[0] \n\tLET " + d + " = (\n\t\t" + "FOR p in " + nName + " \n\t\tFILTER p._id == r._from \n\t\tRETURN p." + function.ByAttribute + "\n\t) " + "\n\tLET " + e + " = " + d + "[0] \n\tRETURN {\n\t\t\"" + f + "\" : " + c + ", \n\t\t" + "\"" + g + "\" : " + e + "\n\t}\n) \n" + "LET function_" + strconv.Itoa(function.TypeID) + " = (\n\tFOR r in " + a + " \n\tCOLLECT c = r." + f + " INTO groups = r." + g + " \n\t\t" + "LET " + h + " = " + function.AppliedModifier + "(groups) \n\t" if len(function.Constraints) > 0 { result += "FILTER " + h + " " + wordsToLogicalSign(function.Constraints[0].MatchType) + " " + function.Constraints[0].Value + " \n\t" } result += "RETURN {\n\t\t" + function.GroupAttribute + " : c, \n\t\t" + function.AppliedModifier + "_" + function.ByAttribute + " : " + h + "\n\t}\n) \n" } } } } result += "RETURN {" if len(functions) > 1 { for l := 0; l < len(functions)-1; l++ { result += "function_" + strconv.Itoa(functions[l].TypeID) + ", " } } result += "function_" + strconv.Itoa(functions[len(functions)-1].TypeID) + "}" return result } func wordsToLogicalSign(word string) string { if word == "LT" { return "<" } else if word == "LTE" { return "<=" } else if word == "EQ" { return "==" } else if word == "GTE" { return ">=" } else { return ">" } }