Newer
Older
/*
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"
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
largestEntityID := len(JSONQuery.Entities) - 1
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
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 {
return nil, errors.New("non-existing relation referenced in return")
result := createQuery(JSONQuery)
/*
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
)
Geurtjens,D. (Douwe Geurtjens)
committed
// 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
for k := range allTypes {
if !includedTypes[k] {
if first {
ret += fmt.Sprintf("WITH %v", k)
first = false
} else {
ret += fmt.Sprintf(", %v", k)
}
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
Geurtjens,D. (Douwe Geurtjens)
committed
// 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)
Geurtjens,D. (Douwe Geurtjens)
committed
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)
Geurtjens,D. (Douwe Geurtjens)
committed
} else if relation.EntityTo >= 0 {
Geurtjens,D. (Douwe Geurtjens)
committed
fmt.Println("Joris are you a madman! How did this happen?")
// if there is only a to-node
Geurtjens,D. (Douwe Geurtjens)
committed
if !entityDone[relation.EntityTo] {
toName := fmt.Sprintf("n%v", relation.EntityTo)
Geurtjens,D. (Douwe Geurtjens)
committed
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)
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)
// 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()
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
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 ">"
}
}