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 {
Geurtjens,D. (Douwe Geurtjens)
committed
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 {
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 {
Geurtjens,D. (Douwe Geurtjens)
committed
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
Geurtjens,D. (Douwe Geurtjens)
committed
if relation.ToID >= 0 && relation.ToType == "entity" {
allTypes[JSONQuery.Entities[relation.ToID].Name] = true
Geurtjens,D. (Douwe Geurtjens)
committed
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
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)
Geurtjens,D. (Douwe Geurtjens)
committed
if relation.FromID >= 0 && relation.FromType == "entity" {
// 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
Geurtjens,D. (Douwe Geurtjens)
committed
if !entityDone[relation.FromID] {
fromName := fmt.Sprintf("n%v", relation.FromID)
Geurtjens,D. (Douwe Geurtjens)
committed
ret += *createNodeLet(&JSONQuery.Entities[relation.FromID], &fromName, &JSONQuery.Filters)
entityDone[relation.FromID] = true
Geurtjens,D. (Douwe Geurtjens)
committed
}
Geurtjens,D. (Douwe Geurtjens)
committed
var function *entity.QueryGroupByStruct
for _, f := range JSONQuery.GroupBys {
Geurtjens,D. (Douwe Geurtjens)
committed
if i == f.RelationID {
function = &f
}
}
Geurtjens,D. (Douwe Geurtjens)
committed
ret += *createRelationLetWithFromEntity(&relation, relationName, &JSONQuery.Entities, JSONQuery.Limit, function, &JSONQuery.Filters)
Geurtjens,D. (Douwe Geurtjens)
committed
Geurtjens,D. (Douwe Geurtjens)
committed
} else if relation.ToID >= 0 && relation.ToType == "entity" {
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.ToID] {
toName := fmt.Sprintf("n%v", relation.ToID)
Geurtjens,D. (Douwe Geurtjens)
committed
ret += *createNodeLet(&JSONQuery.Entities[relation.ToID], &toName, &JSONQuery.Filters)
entityDone[relation.ToID] = true
Geurtjens,D. (Douwe Geurtjens)
committed
}
Geurtjens,D. (Douwe Geurtjens)
committed
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 {
Geurtjens,D. (Douwe Geurtjens)
committed
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)
Geurtjens,D. (Douwe Geurtjens)
committed
ret += *createNodeLet(&JSONQuery.Entities[entityIndex], &name, &JSONQuery.Filters)
// Add this node to the list
nodesToReturn = append(nodesToReturn, name)
}
}
Geurtjens,D. (Douwe Geurtjens)
committed
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
Geurtjens,D. (Douwe Geurtjens)
committed
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)
pathDistinction = ".edges[**]"
// Getting the attribute if there is one
if modifier.AttributeIndex != -1 {
if modifier.SelectedType == "entity" {
Geurtjens,D. (Douwe Geurtjens)
committed
nodeFilters := GetFiltersOnNode(&JSONQuery.Entities[modifier.SelectedTypeID], &JSONQuery.Filters)
pathDistinction += fmt.Sprintf(".%v", (*nodeFilters)[modifier.AttributeIndex].Attribute)
} else {
Geurtjens,D. (Douwe Geurtjens)
committed
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)
// 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" {
Geurtjens,D. (Douwe Geurtjens)
committed
nodeFilters := GetFiltersOnNode(&JSONQuery.Entities[modifier.SelectedTypeID], &JSONQuery.Filters)
attribute = (*nodeFilters)[modifier.AttributeIndex].Attribute
} else {
Geurtjens,D. (Douwe Geurtjens)
committed
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 }"
Geurtjens,D. (Douwe Geurtjens)
committed
} else {
Geurtjens,D. (Douwe Geurtjens)
committed
ret += createTableWithFunctions(&JSONQuery.GroupBys, &JSONQuery.Relations, &JSONQuery.Entities, &JSONQuery.Filters)
}
return &ret
}
Geurtjens,D. (Douwe Geurtjens)
committed
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
/*
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
Geurtjens,D. (Douwe Geurtjens)
committed
func createNodeLet(node *entity.QueryEntityStruct, name *string, filters *[]entity.QueryFilterStruct) *string {
header := createLetFor(*name, "x", node.Name)
footer := "\tRETURN x\n)\n"
Geurtjens,D. (Douwe Geurtjens)
committed
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
Geurtjens,D. (Douwe Geurtjens)
committed
// 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 := ""
Geurtjens,D. (Douwe Geurtjens)
committed
if relation.ToType == "entity" {
// If there is a to-node, generate the filter statement
Geurtjens,D. (Douwe Geurtjens)
committed
toFilters := GetFiltersOnNode(&(*entities)[relation.ToID], filters)
vFilterStmnt += *createConstraintStatements(toFilters, "v", false)
Geurtjens,D. (Douwe Geurtjens)
committed
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
Geurtjens,D. (Douwe Geurtjens)
committed
// 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"
Geurtjens,D. (Douwe Geurtjens)
committed
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
}
Geurtjens,D. (Douwe Geurtjens)
committed
func createTableWithFunctions(functions *[]entity.QueryGroupByStruct, relations *[]entity.QueryRelationStruct, entities *[]entity.QueryEntityStruct, filters *[]entity.QueryFilterStruct) string {
result := ""
v := newVariableNameGeneratorToken()
Geurtjens,D. (Douwe Geurtjens)
committed
for _, function := range *functions {
currRelation := (*relations)[function.RelationID]
if true {
Geurtjens,D. (Douwe Geurtjens)
committed
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)
Geurtjens,D. (Douwe Geurtjens)
committed
result += createFunction(function, a, f, g, h, filters)
}
Geurtjens,D. (Douwe Geurtjens)
committed
}
result += "RETURN {"
Geurtjens,D. (Douwe Geurtjens)
committed
if len(*functions) > 1 {
for l := 0; l < len(*functions)-1; l++ {
result += "function_" + strconv.Itoa((*functions)[l].ID) + ", "
}
}
Geurtjens,D. (Douwe Geurtjens)
committed
result += "function_" + strconv.Itoa((*functions)[len(*functions)-1].ID) + "}"
return result
}
Geurtjens,D. (Douwe Geurtjens)
committed
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 {
Geurtjens,D. (Douwe Geurtjens)
committed
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
}
Geurtjens,D. (Douwe Geurtjens)
committed
func createFunction(function entity.QueryGroupByStruct, variableName1 string, variableName2 string, variableName3 string, variableName4 string, filters *[]entity.QueryFilterStruct) string {
Geurtjens,D. (Douwe Geurtjens)
committed
result += createLetFor("function_"+strconv.Itoa(function.ID), "r", variableName1)
result += createCollect("c", "r."+variableName2, "r."+variableName3, variableName4, function)
Geurtjens,D. (Douwe Geurtjens)
committed
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"
}
Geurtjens,D. (Douwe Geurtjens)
committed
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"
}
Geurtjens,D. (Douwe Geurtjens)
committed
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 ">"
}
}