Skip to content
Snippets Groups Projects
Commit ab14486b authored by Fjodor's avatar Fjodor
Browse files

Started adding cypher converter @Lorenzo

parent 06ff65cd
No related branches found
No related tags found
No related merge requests found
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
}
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
}
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{}
}
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)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment