Newer
Older
package aql
import (
"errors"
"fmt"
"git.science.uu.nl/datastrophe/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)
return result, nil
}
/* createQuery generates a query based on the json file provided
Parameters: jsonQuery is a parsedJSON struct holding all the data needed to form a query
Return: 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
)
// 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] {
ret += fmt.Sprintf("WITH %v\n", 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
fromName := fmt.Sprintf("n%v", relation.EntityFrom)
ret += *createNodeLet(&JSONQuery.Entities[relation.EntityFrom], &fromName)
ret += *createRelationLetWithFromEntity(&relation, relationName, &JSONQuery.Entities, JSONQuery.Limit)
} else if relation.EntityTo >= 0 {
// if there is only a to-node
toName := fmt.Sprintf("n%v", relation.EntityTo)
ret += *createNodeLet(&JSONQuery.Entities[relation.EntityTo], &toName)
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 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]"
// 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" {
pathDistinction += fmt.Sprintf(".%v", JSONQuery.Entities[modifier.SelectedTypeID].Constraints[modifier.AttributeIndex].Attribute)
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)
}
} 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" {
attribute = JSONQuery.Entities[modifier.SelectedTypeID].Constraints[modifier.AttributeIndex].Attribute
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)
ret += fmt.Sprintf("RETURN %v (n%v[*].%v)", modifier.Type, modifier.SelectedTypeID, attribute)
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
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
}
}
}
} 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 }"
}
return &ret
}
/* createNodeLet generates a 'LET' statement for a node related query
Parameters: node is an entityStruct containing the information of a single node,
name is the autogenerated name of the node consisting of "n" + the index of the node
Return: 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
Parameters: relation is a relation struct containing the information of a single relation,
name is the autogenerated name of the node consisting of "r" + the index of the relation,
entities is a list of entityStructs that are needed to form the relation LET-statement
Return: a string containing a single LET-statement in AQL
*/
func createRelationLetWithFromEntity(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.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)
}
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
Parameters: relation is a relation struct containing the information of a single relation,
name is the autogenerated name of the node consisting of "r" + the index of the relation,
entities is a list of entityStructs that are needed to form the relation LET-statement
Return: 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
}