/* 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.FromID > largestEntityID && r.FromType == "entity") || (r.ToID > largestEntityID && r.ToType == "entity") { 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") } } search(JSONQuery) 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.FromID >= 0 && relation.FromType == "entity" { includedTypes[JSONQuery.Entities[relation.FromID].Name] = true allTypes[JSONQuery.Entities[relation.FromID].Name] = true // If the type is in the entityTo it is a valid type but not yet included if relation.ToID >= 0 && relation.ToType == "entity" { allTypes[JSONQuery.Entities[relation.ToID].Name] = true } } if relation.FromType != "entity" && relation.ToID >= 0 && relation.ToType == "entity" { includedTypes[JSONQuery.Entities[relation.ToID].Name] = true allTypes[JSONQuery.Entities[relation.ToID].Name] = 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.FromID >= 0 && relation.FromType == "entity" { // 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.FromID] { fromName := fmt.Sprintf("n%v", relation.FromID) ret += *createNodeLet(&JSONQuery.Entities[relation.FromID], &fromName, &JSONQuery.Filters) entityDone[relation.FromID] = true } var function *entity.QueryGroupByStruct for _, f := range JSONQuery.GroupBys { if i == f.RelationID { function = &f } } ret += *createRelationLetWithFromEntity(&relation, relationName, &JSONQuery.Entities, JSONQuery.Limit, function, &JSONQuery.Filters) } else if relation.ToID >= 0 && relation.ToType == "entity" { fmt.Println("Joris are you a madman! How did this happen?") // if there is only a to-node if !entityDone[relation.ToID] { toName := fmt.Sprintf("n%v", relation.ToID) ret += *createNodeLet(&JSONQuery.Entities[relation.ToID], &toName, &JSONQuery.Filters) entityDone[relation.ToID] = true } ret += *createRelationLetWithOnlyToEntity(&relation, relationName, &JSONQuery.Entities, JSONQuery.Limit, &JSONQuery.Filters) // 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 { if relation.FromType == "entity" { nodeSet[relation.FromID] = true } if relation.ToType == "entity" { nodeSet[relation.ToID] = 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, &JSONQuery.Filters) // Add this node to the list nodesToReturn = append(nodesToReturn, name) } } if len(JSONQuery.GroupBys) == 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].FromID == modifier.SelectedTypeID && JSONQuery.Relations[0].FromType == "entity" { // 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" { nodeFilters := GetFiltersOnNode(&JSONQuery.Entities[modifier.SelectedTypeID], &JSONQuery.Filters) pathDistinction += fmt.Sprintf(".%v", (*nodeFilters)[modifier.AttributeIndex].Attribute) } else { relationFilters := GetFiltersOnRelation(&JSONQuery.Relations[modifier.SelectedTypeID], &JSONQuery.Filters) pathDistinction += fmt.Sprintf(".%v", (*relationFilters)[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" { nodeFilters := GetFiltersOnNode(&JSONQuery.Entities[modifier.SelectedTypeID], &JSONQuery.Filters) attribute = (*nodeFilters)[modifier.AttributeIndex].Attribute } else { relationFilters := GetFiltersOnRelation(&JSONQuery.Relations[modifier.SelectedTypeID], &JSONQuery.Filters) attribute = (*relationFilters)[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.GroupBys, &JSONQuery.Relations, &JSONQuery.Entities, &JSONQuery.Filters) } return &ret } /* GetFiltersOnNode gets all the filters on a certain node node: *entity.QueryEntityStruct, node is an entityStruct containing the information of a single node, JSONQuery: *entity.IncomingQueryJSON, the full incoming query, Return: *[]entity.QueryFilterStruct, a list of filters attached to the node */ func GetFiltersOnNode(node *entity.QueryEntityStruct, filters *[]entity.QueryFilterStruct) *[]entity.QueryFilterStruct { var retval []entity.QueryFilterStruct for _, filter := range *filters { if filter.FilteredType == "entity" && filter.FilteredID == node.ID { retval = append(retval, filter) } } return &retval } func GetFiltersOnRelation(relation *entity.QueryRelationStruct, filters *[]entity.QueryFilterStruct) *[]entity.QueryFilterStruct { var retval []entity.QueryFilterStruct for _, filter := range *filters { if filter.FilteredType == "entity" && filter.FilteredID == relation.ID { retval = append(retval, filter) } } return &retval } func GetFiltersOnFunction(function *entity.QueryGroupByStruct, filters *[]entity.QueryFilterStruct) *[]entity.QueryFilterStruct { var retval []entity.QueryFilterStruct for _, filter := range *filters { if filter.FilteredType == "entity" && filter.FilteredID == function.ID { retval = append(retval, filter) } } return &retval } /* 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, filters *[]entity.QueryFilterStruct) *string { header := createLetFor(*name, "x", node.Name) footer := "\tRETURN x\n)\n" attachedfilters := GetFiltersOnNode(node, filters) constraints := *createConstraintStatements(attachedfilters, "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 */ // JSON FORMAT CHANGES COMMENT // THIS ONLY GETS CALLED WHEN THE RELATION FROM HAS ALREADY BEEN VERIFIED TO BE AN ENTITY // THAT IS WHY WE CAN USE FROMID WITHOUT CHECKING IF IT IS AN ENTITY BEFOREHAND func createRelationLetWithFromEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int, function *entity.QueryGroupByStruct, filters *[]entity.QueryFilterStruct) *string { header := createLetFor(name, "x", "n"+strconv.Itoa(relation.FromID)) forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v OUTBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Name) // 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.ToType == "entity" { // If there is a to-node, generate the filter statement toFilters := GetFiltersOnNode(&(*entities)[relation.ToID], filters) vFilterStmnt += *createConstraintStatements(toFilters, "v", false) } relationFilters := GetFiltersOnRelation(relation, filters) relationFilterStmnt := *createConstraintStatements(relationFilters, "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 */ // JSON FORMAT CHANGES COMMENT // THIS ONLY GETS CALLED WHEN THE RELATION FROM HAS ALREADY BEEN VERIFIED TO BE AN ENTITY // THAT IS WHY WE CAN USE FROMID WITHOUT CHECKING IF IT IS AN ENTITY BEFOREHAND func createRelationLetWithOnlyToEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int, filters *[]entity.QueryFilterStruct) *string { header := createLetFor(name, "x", "n"+strconv.Itoa(relation.ToID)) forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v INBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Name) // 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" relationFilters := GetFiltersOnRelation(relation, filters) relationFilterStmnt := *createConstraintStatements(relationFilters, "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 } func createLetFor(variableName string, iteratorName string, enumerableName string) string { return "LET " + variableName + " = (\n\tFOR " + iteratorName + " IN " + enumerableName + " \n" } 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.QueryGroupByStruct, relations *[]entity.QueryRelationStruct, entities *[]entity.QueryEntityStruct, filters *[]entity.QueryFilterStruct) string { result := "" v := newVariableNameGeneratorToken() for _, function := range *functions { currRelation := (*relations)[function.RelationID] if true { 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", function.RelationID) nName := fmt.Sprintf("n%v", function.GroupID) result += createTupleVariable(a, b, c, d, e, f, g, currRelation, rName, nName, function) result += createFunction(function, a, f, g, h, filters) } } result += "RETURN {" if len(*functions) > 1 { for l := 0; l < len(*functions)-1; l++ { result += "function_" + strconv.Itoa((*functions)[l].ID) + ", " } } result += "function_" + strconv.Itoa((*functions)[len(*functions)-1].ID) + "}" return result } func createTupleVariable(a string, b string, c string, d string, e string, f string, g string, currRelation entity.QueryRelationStruct, forName1 string, forName2 string, function entity.QueryGroupByStruct) string { result := "" result += createLetFor(a, "r", currRelation.Name) result += createSubVariable(b, c, forName1, "_id", "r._to", function.ByAttribute) result += createSubVariable(d, e, forName2, "_id", "r._from", function.GroupAttribute) result += createTupleReturn(c, e, "\""+f+"\"", "\""+g+"\"") return result } func createFunction(function entity.QueryGroupByStruct, variableName1 string, variableName2 string, variableName3 string, variableName4 string, filters *[]entity.QueryFilterStruct) string { result := "" result += createLetFor("function_"+strconv.Itoa(function.ID), "r", variableName1) result += createCollect("c", "r."+variableName2, "r."+variableName3, variableName4, function) functionFilters := GetFiltersOnFunction(&function, filters) if len(*functionFilters) > 0 { result += createFilter(variableName4, function, filters) } result += createTupleReturn("c", variableName4, function.ByAttribute, function.AppliedModifier+"_"+function.GroupAttribute) return result } func createSubVariable(variableName string, variableName2 string, forName string, filter1 string, filter2 string, returnValue string) string { result := "\t" result += createLetFor(variableName, "c", forName) return result + "\t\tFILTER c." + filter1 + " == " + filter2 + "\n\t\tRETURN c." + returnValue + "\n\t) " + "\n\tLET " + variableName2 + " = " + variableName + "[0] \n" } func createTupleReturn(assignVariable1 string, assignVariable2 string, variableName1 string, variableName2 string) string { return "\tRETURN {\n\t\t" + variableName1 + " : " + assignVariable1 + ", \n\t\t" + "" + variableName2 + " : " + assignVariable2 + "\n\t}\n) \n" } func createCollect(collectionName string, variableName1 string, variableName2 string, variableName3 string, function entity.QueryGroupByStruct) string { return "\tCOLLECT " + collectionName + " = " + variableName1 + " INTO groups = " + variableName2 + " \n\t\t" + "LET " + variableName3 + " = " + function.AppliedModifier + "(groups) \n\t" } func createFilter(variableName string, function entity.QueryGroupByStruct, filters *[]entity.QueryFilterStruct) string { functionFilters := GetFiltersOnFunction(&function, filters) return "FILTER " + variableName + " " + wordsToLogicalSign((*functionFilters)[0].MatchType) + " " + (*functionFilters)[0].Value + " \n\t" } 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 ">" } }