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")
//search(JSONQuery)
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 := createLetFor(*name, "x", 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 := createLetFor(name, "x", "n"+strconv.Itoa(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 := createLetFor(name, "x", "n"+strconv.Itoa(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
}
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.QueryFunctionStruct, relations []entity.QueryRelationStruct, entities []entity.QueryEntityStruct) string {
result := ""
v := newVariableNameGeneratorToken()
for _, function := range functions {
Geurtjens,D. (Douwe Geurtjens)
committed
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 += createTupleVariable(a, b, c, d, e, f, g, currRelation, rName, nName, function)
result += createFunction(function, a, f, g, h)
}
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
}
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
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.QueryFunctionStruct) string {
result := ""
result += createLetFor(a, "r", currRelation.Type)
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.QueryFunctionStruct, variableName1 string, variableName2 string, variableName3 string, variableName4 string) string {
result := ""
result += createLetFor("function_"+strconv.Itoa(function.TypeID), "r", variableName1)
result += createCollect("c", "r."+variableName2, "r."+variableName3, variableName4, function)
if len(function.Constraints) > 0 {
result += createFilter(variableName4, function)
}
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.QueryFunctionStruct) string {
return "\tCOLLECT " + collectionName + " = " + variableName1 + " INTO groups = " + variableName2 + " \n\t\t" +
"LET " + variableName3 + " = " + function.AppliedModifier + "(groups) \n\t"
}
func createFilter(variableName string, function entity.QueryFunctionStruct) string {
return "FILTER " + variableName + " " + wordsToLogicalSign(function.Constraints[0].MatchType) + " " + function.Constraints[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 ">"
}
}