Skip to content
Snippets Groups Projects
convertQuery.go 11.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • LoLo5689's avatar
    LoLo5689 committed
    /*
    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"
    
    
    	"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
    
    	// The largest possible id for an entity
    
    	largestEntityID := len(JSONQuery.Entities) - 1
    
    	// The largest possible id for a relation
    
    	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
    
    		if e > largestEntityID || e < 0 {
    
    			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 {
    
    		if r > largestRelationID || r < 0 {
    
    			return nil, errors.New("non-existing relation referenced in return")
    
    	result := createQuery(JSONQuery)
    	return result, nil
    
    LoLo5689's avatar
    LoLo5689 committed
    /*
    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
    	)
    
    	// 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)
    
    
    				}
    			}
    		}
    
    	} 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
    }
    
    
    LoLo5689's avatar
    LoLo5689 committed
    /*
    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
    }
    
    
    LoLo5689's avatar
    LoLo5689 committed
    /*
    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) *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
    }
    
    
    LoLo5689's avatar
    LoLo5689 committed
    /*
    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
    }