package aql import ( "errors" "fmt" "git.science.uu.nl/datastrophe/query-conversion/entity" ) /* 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 Parameters: jsonQuery is a parsedJSON struct holding all the data needed to form a query Return: 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 ) // Loop over all relations ret := "" 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 fromName := fmt.Sprintf("n%v", relation.EntityFrom) ret += *createNodeLet(&JSONQuery.Entities[relation.EntityFrom], &fromName) 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) ret += *createNodeLet(&JSONQuery.Entities[relation.EntityTo], &toName) 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 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 = "\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 }" } return &ret } /* createNodeLet generates a 'LET' statement for a node related query Parameters: node is an entityStruct containing the information of a single node, name is the autogenerated name of the node consisting of "n" + the index of the node Return: 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 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 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("%v\n\tWITH %v", header, (*entities)[(*relation).EntityTo].Type) } } 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 + 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" 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 }