Skip to content
Snippets Groups Projects
convertQuery.go 19.8 KiB
Newer Older
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/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
	// 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.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 {
		if r > largestRelationID || r < 0 {
			return nil, errors.New("non-existing relation referenced in return")
	search(JSONQuery)
	result := createQuery(JSONQuery)
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
	)
	// 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.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
			if relation.ToID >= 0 && relation.ToType == "entity" {
				allTypes[JSONQuery.Entities[relation.ToID].Name] = true
		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
LoLo5689's avatar
LoLo5689 committed
	first := true
	for k := range allTypes {
		if !includedTypes[k] {
LoLo5689's avatar
LoLo5689 committed
			if first {
				ret += fmt.Sprintf("WITH %v", k)
				first = false
			} else {
				ret += fmt.Sprintf(", %v", k)
			}
LoLo5689's avatar
LoLo5689 committed
	if !first {
		ret += "\n"
	}
	for i, relation := range JSONQuery.Relations {

		relationName := fmt.Sprintf("r%v", i)

		if relation.FromID >= 0 && relation.FromType == "entity" {
			// if there is a from-node
			// create the let for this node
			// IF WE'VE ALREADY SEEN THIS ENTITY WE DON'T HAVE TO REQUERY IT, WE CAN JUST REUSE THE LET BINDING
			if !entityDone[relation.FromID] {
				fromName := fmt.Sprintf("n%v", relation.FromID)
				ret += *createNodeLet(&JSONQuery.Entities[relation.FromID], &fromName, &JSONQuery.Filters)
				entityDone[relation.FromID] = true
			var function *entity.QueryGroupByStruct
			for _, f := range JSONQuery.GroupBys {
			ret += *createRelationLetWithFromEntity(&relation, relationName, &JSONQuery.Entities, JSONQuery.Limit, function, &JSONQuery.Filters)
		} else if relation.ToID >= 0 && relation.ToType == "entity" {
			fmt.Println("Joris are you a madman! How did this happen?")
			// if there is only a to-node
			if !entityDone[relation.ToID] {
				toName := fmt.Sprintf("n%v", relation.ToID)
				ret += *createNodeLet(&JSONQuery.Entities[relation.ToID], &toName, &JSONQuery.Filters)
				entityDone[relation.ToID] = true
			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 {
		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)
			ret += *createNodeLet(&JSONQuery.Entities[entityIndex], &name, &JSONQuery.Filters)

			// 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].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)
				// Getting the attribute if there is one
				if modifier.AttributeIndex != -1 {
					if modifier.SelectedType == "entity" {
						nodeFilters := GetFiltersOnNode(&JSONQuery.Entities[modifier.SelectedTypeID], &JSONQuery.Filters)
						pathDistinction += fmt.Sprintf(".%v", (*nodeFilters)[modifier.AttributeIndex].Attribute)
						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" {
						nodeFilters := GetFiltersOnNode(&JSONQuery.Entities[modifier.SelectedTypeID], &JSONQuery.Filters)
						attribute = (*nodeFilters)[modifier.AttributeIndex].Attribute
						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)
			// 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 }"
		ret += createTableWithFunctions(&JSONQuery.GroupBys, &JSONQuery.Relations, &JSONQuery.Entities, &JSONQuery.Filters)
/*
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
}

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, filters *[]entity.QueryFilterStruct) *string {
	header := createLetFor(*name, "x", node.Name)
	footer := "\tRETURN x\n)\n"
	attachedfilters := GetFiltersOnNode(node, filters)
	constraints := *createConstraintStatements(attachedfilters, "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
// 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 := ""
		// If there is a to-node, generate the filter statement
		toFilters := GetFiltersOnNode(&(*entities)[relation.ToID], filters)
		vFilterStmnt += *createConstraintStatements(toFilters, "v", false)
	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
}

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
// 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"

	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 " +
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.QueryGroupByStruct, relations *[]entity.QueryRelationStruct, entities *[]entity.QueryEntityStruct, filters *[]entity.QueryFilterStruct) string {
	result := ""
	v := newVariableNameGeneratorToken()
	for _, function := range *functions {
		currRelation := (*relations)[function.RelationID]
		if true {
			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, filters)
	if len(*functions) > 1 {
		for l := 0; l < len(*functions)-1; l++ {
			result += "function_" + strconv.Itoa((*functions)[l].ID) + ", "
	result += "function_" + strconv.Itoa((*functions)[len(*functions)-1].ID) + "}"
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 {
	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.QueryGroupByStruct, variableName1 string, variableName2 string, variableName3 string, variableName4 string, filters *[]entity.QueryFilterStruct) string {
	result += createLetFor("function_"+strconv.Itoa(function.ID), "r", variableName1)
	result += createCollect("c", "r."+variableName2, "r."+variableName3, variableName4, function)
	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"
}

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"
}

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 ">"
	}
}