diff --git a/cypher/convertQuery.go b/cypher/convertQuery.go new file mode 100644 index 0000000000000000000000000000000000000000..059570d571937ec89a52bb64d98e18d52b30b59e --- /dev/null +++ b/cypher/convertQuery.go @@ -0,0 +1,297 @@ +package cypher + +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 + // How many entities are there + numEntities := len(JSONQuery.Entities) - 1 + // How many relations there are + numRelations := 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 > numEntities || 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 > numEntities || r.EntityTo > numEntities { + 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 > numRelations || r < 0 { + 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 := "" + + 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.ID { + pathDistinction = fmt.Sprintf(".vertices[%v]", JSONQuery.Relations[0].Depth.Min-1) + + } else { + 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.ID].Constraints[modifier.AttributeIndex].Attribute) + + } else { + pathDistinction += fmt.Sprintf(".%v", JSONQuery.Relations[modifier.ID].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.ID) + } else { + var attribute string + + // Selecting the right attribute from either the entity constraint or relation constraint + if modifier.SelectedType == "entity" { + attribute = JSONQuery.Entities[modifier.ID].Constraints[modifier.AttributeIndex].Attribute + + } else { + attribute = JSONQuery.Relations[modifier.ID].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.ID, attribute) + + } else { + ret += fmt.Sprintf("RETURN %v (n%v[*].%v)", modifier.Type, modifier.ID, 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 +} + +/* 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("MATCH (%v : %v)\n", *name, node.Type) + constraints := *createConstraintStatements(&node.Constraints, *name, false) + ret := header + constraints + 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) + + // Add a WITH statement if the collection of entityTo is not yet included + if (*entities)[(*relation).EntityFrom].Type != (*entities)[(*relation).EntityTo].Type { + header = fmt.Sprintf("WITH %v\n %v", (*entities)[(*relation).EntityTo].Type, header) + } + } + + 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 +} diff --git a/cypher/createConstraints.go b/cypher/createConstraints.go new file mode 100644 index 0000000000000000000000000000000000000000..7bd06270da18c5ef96e1fee6a9cbab7150f7e9c5 --- /dev/null +++ b/cypher/createConstraints.go @@ -0,0 +1,104 @@ +package cypher + +import ( + "fmt" + + "git.science.uu.nl/datastrophe/query-conversion/entity" +) + +/* createConstraintStatements generates the appropriate amount of constraint lines calling createConstraingBoolExpression +Parameters: constraints is a list of constraintStructs that specify the constraints of a node or relation, +name is the id of the corresponding relation/node, +isRelation is a boolean specifying if this constraint comes from a node or relation + +Return: a string containing a FILTER-statement with all the constraints +*/ +func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, name string, isRelation bool) *string { + s := "" + if len(*constraints) == 0 { + return &s + } + + newLineStatement := "\tWHERE" + + for _, v := range *constraints { + s += fmt.Sprintf("%v %v \n", newLineStatement, *createConstraintBoolExpression(&v, name, isRelation)) + newLineStatement = "\tAND" + } + + return &s +} + +/* createConstraintBoolExpression generates a single boolean expression, +e.g. {name}.city == "New York". + +Parameters: constraint is a single constraint of a node or relation, +name is the id of the corresponding relation/node, +isRelation is a boolean specifying if this constraint comes from a node or relation, that changes the structure of the expression + +Return: a string containing an boolean expression of a single constraint +*/ +func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, name string, isRelation bool) *string { + var ( + match string + value string + line string + neq string + ) + + // Constraint datatypes back end + // text MatchTypes: EQ/NEQ/contains/excludes + // number MatchTypes: EQ/NEQ/GT/LT/GET/LET + // bool MatchTypes: EQ/NEQ + + neq = "" + + switch constraint.DataType { + case "text": + value = fmt.Sprintf("\"%s\"", constraint.Value) + switch constraint.MatchType { + case "NEQ": + match = "<>" + case "contains": + match = "CONTAINS" + value = fmt.Sprintf("\"%%%s%%\"", constraint.Value) + case "excludes": + match = "CONTAINS" + value = fmt.Sprintf("\"%%%s%%\"", constraint.Value) + neq = "NOT" + default: //EQ + match = "=" + } + case "number": + value = constraint.Value + switch constraint.MatchType { + case "NEQ": + match = "<>" + case "GT": + match = ">" + case "LT": + match = "<" + case "GET": + match = ">=" + case "LET": + match = "<=" + default: //EQ + match = "=" + } + default: /*bool*/ + value = constraint.Value + switch constraint.MatchType { + case "NEQ": + match = "<>" + default: //EQ + match = "=" + } + } + + if isRelation { + line = fmt.Sprintf("%s.edges[*].%s ALL %s %s", name, constraint.Attribute, match, value) + } else { + line = fmt.Sprintf("%s %s.%s %s %s", neq, name, constraint.Attribute, match, value) + } + return &line +} diff --git a/cypher/queryConverter.go b/cypher/queryConverter.go new file mode 100644 index 0000000000000000000000000000000000000000..1b0536f033d0d218777d633f84d0f67664b7fbb0 --- /dev/null +++ b/cypher/queryConverter.go @@ -0,0 +1,10 @@ +package cypher + +// Service implements the QueryConverter interface (in the query service) +type Service struct { +} + +// NewService creates a new AQL conversion service +func NewService() *Service { + return &Service{} +} diff --git a/main/main.go b/main/main.go new file mode 100644 index 0000000000000000000000000000000000000000..9ccfa23f680b33600685dd2a7f1d226d50d76190 --- /dev/null +++ b/main/main.go @@ -0,0 +1,38 @@ +package main + +import( + "log" + "git.science.uu.nl/datastrophe/query-conversion/cypher/queryConverter.go" +) + +func main(){ + queryservice := new cypher.NewService() + + js = `{ + "return": { + "entities": [ + 0 + ], + "relations": [] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [], + "limit": 5000 + }` + + + result, _ := cypher.convertQuery(JSON.marshal(js)) + log.Println(result) +}