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 {
Geurtjens,D. (Douwe Geurtjens)
committed
if i == f.RelationID {
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()
for _, function := range functions {
Geurtjens,D. (Douwe Geurtjens)
committed
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
currRelation := relations[function.RelationID]
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", function.RelationID)
nName := fmt.Sprintf("n%v", function.GroupID)
result += "LET " + a + " = (\n\tFOR r IN " +
currRelation.Type + "\n\tLET " + b +
" = (\n\t\tFOR c IN " + rName +
" \n\t\tFILTER c._id == r._to \n\t\tRETURN c." + function.ByAttribute + "\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.GroupAttribute + "\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"
}
Geurtjens,D. (Douwe Geurtjens)
committed
result += "RETURN {\n\t\t" + function.ByAttribute + " : c, \n\t\t" +
function.AppliedModifier + "_" + function.GroupAttribute + " : " + h + "\n\t}\n) \n"
}
Geurtjens,D. (Douwe Geurtjens)
committed
}
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 ">"
}
}