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

Added relations, relations constraints, removed modifiers @Lorenzo

parent 4ad036ac
No related branches found
No related tags found
No related merge requests found
...@@ -3,6 +3,7 @@ package cypher ...@@ -3,6 +3,7 @@ package cypher
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"git.science.uu.nl/datastrophe/query-conversion/entity" "git.science.uu.nl/datastrophe/query-conversion/entity"
) )
...@@ -46,6 +47,23 @@ func (s *Service) ConvertQuery(JSONQuery *entity.IncomingQueryJSON) (*string, er ...@@ -46,6 +47,23 @@ func (s *Service) ConvertQuery(JSONQuery *entity.IncomingQueryJSON) (*string, er
return result, nil return result, nil
} }
func sliceContains(s []int, e int) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
/*TrimSuffix trims the final character of a string */
func TrimSuffix(s, suffix string) string {
if strings.HasSuffix(s, suffix) {
s = s[:len(s)-len(suffix)]
}
return s
}
/* createQuery generates a query based on the json file provided /* 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 Parameters: jsonQuery is a parsedJSON struct holding all the data needed to form a query
...@@ -64,42 +82,84 @@ func createQuery(JSONQuery *entity.IncomingQueryJSON) *string { ...@@ -64,42 +82,84 @@ func createQuery(JSONQuery *entity.IncomingQueryJSON) *string {
nodesToReturn []string nodesToReturn []string
nodeUnion string nodeUnion string
relationUnion string relationUnion string
queryList [][][]int
entityList []int
ret string
) )
// Loop over all relations
ret := ""
for i, relation := range JSONQuery.Relations { for i, relation := range JSONQuery.Relations {
var contains bool
contains = false
for j := range queryList {
if sliceContains(queryList[j][0], relation.EntityFrom) || sliceContains(queryList[j][0], relation.EntityTo) {
if !sliceContains(queryList[j][0], relation.EntityFrom) {
queryList[j][0] = append(queryList[j][0], relation.EntityFrom)
entityList = append(entityList, relation.EntityFrom)
}
if !sliceContains(queryList[j][0], relation.EntityTo) {
queryList[j][0] = append(queryList[j][0], relation.EntityTo)
entityList = append(entityList, relation.EntityTo)
}
queryList[j][1] = append(queryList[j][1], i)
contains = true
}
}
if !contains {
queryList = append(queryList, [][]int{{relation.EntityFrom, relation.EntityTo}, {i}})
}
}
relationName := fmt.Sprintf("r%v", i) for i := range queryList {
//reset variables for the next query
if relation.EntityFrom >= 0 { nodeUnion = ""
// if there is a from-node relationUnion = ""
// create the let for this node relationsToReturn = []string{}
fromName := fmt.Sprintf("n%v", relation.EntityFrom) for j, relationID := range queryList[i][1] {
relationName := fmt.Sprintf("r%v", j)
relation := JSONQuery.Relations[relationID]
pathName := fmt.Sprintf("p%v", j)
relationsToReturn = append(relationsToReturn, pathName)
if relation.EntityFrom >= 0 {
// if there is a from-node
// create the let for this node
fromName := fmt.Sprintf("n%v", relation.EntityFrom)
ret += *createNodeMatch(&JSONQuery.Entities[relation.EntityFrom], &fromName)
ret += *createRelationMatch(&relation, relationName, pathName, &JSONQuery.Entities, JSONQuery.Limit, true)
} else if relation.EntityTo >= 0 {
// if there is only a to-node
toName := fmt.Sprintf("n%v", relation.EntityTo)
ret += *createNodeMatch(&JSONQuery.Entities[relation.EntityTo], &toName)
ret += *createRelationMatch(&relation, relationName, pathName, &JSONQuery.Entities, JSONQuery.Limit, false)
// Add this relation to the list
} else {
fmt.Println("Relation-only queries are currently not supported")
continue
}
}
ret += *createNodeLet(&JSONQuery.Entities[relation.EntityFrom], &fromName) // Create UNION statements that create unique lists of all the nodes and relations
ret += *createRelationLetWithFromEntity(&relation, relationName, &JSONQuery.Entities, JSONQuery.Limit) // Thus removing all duplicates
} else if relation.EntityTo >= 0 { nodeUnion = "RETURN "
// if there is only a to-node
toName := fmt.Sprintf("n%v", relation.EntityTo)
ret += *createNodeLet(&JSONQuery.Entities[relation.EntityTo], &toName) for _, entityID := range queryList[i][0] {
if sliceContains(JSONQuery.Return.Entities, entityID) {
nodeUnion += fmt.Sprintf("n%v,", entityID)
}
}
ret += *createRelationLetWithOnlyToEntity(&relation, relationName, &JSONQuery.Entities, JSONQuery.Limit) for _, relation := range relationsToReturn {
// Add this relation to the list relationUnion += fmt.Sprintf("%v,", relation)
} else {
fmt.Println("Relation-only queries are currently not supported")
continue
} }
// Add this relation to the list relationUnion = TrimSuffix(relationUnion, ",")
relationsToReturn = append(relationsToReturn, relationName) ret += nodeUnion + relationUnion + ";\n"
} }
// 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) nodeSet := make(map[int]bool)
for _, relation := range JSONQuery.Relations { for _, relation := range JSONQuery.Relations {
nodeSet[relation.EntityFrom] = true nodeSet[relation.EntityFrom] = true
...@@ -111,103 +171,13 @@ func createQuery(JSONQuery *entity.IncomingQueryJSON) *string { ...@@ -111,103 +171,13 @@ func createQuery(JSONQuery *entity.IncomingQueryJSON) *string {
if !nodeSet[entityIndex] { if !nodeSet[entityIndex] {
// If not, return this node // If not, return this node
name := fmt.Sprintf("n%v", entityIndex) name := fmt.Sprintf("n%v", entityIndex)
ret += *createNodeLet(&JSONQuery.Entities[entityIndex], &name) ret += *createNodeMatch(&JSONQuery.Entities[entityIndex], &name)
// Add this node to the list // Add this node to the list
nodesToReturn = append(nodesToReturn, name) 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 = "\nRETURN "
for _, node := range nodesToReturn {
nodeUnion += fmt.Sprintf("%v,", node)
}
// RETURN n0, n1, n2, nn, r0, r1, r2, r3, rn
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 return &ret
} }
...@@ -217,9 +187,9 @@ name is the autogenerated name of the node consisting of "n" + the index of the ...@@ -217,9 +187,9 @@ name is the autogenerated name of the node consisting of "n" + the index of the
Return: a string containing a single LET-statement in AQL Return: a string containing a single LET-statement in AQL
*/ */
func createNodeLet(node *entity.QueryEntityStruct, name *string) *string { func createNodeMatch(node *entity.QueryEntityStruct, name *string) *string {
header := fmt.Sprintf("MATCH (%v : %v)\n", *name, node.Type) header := fmt.Sprintf("MATCH (%v:%v)\n", *name, node.Type)
constraints := *createConstraintStatements(&node.Constraints, *name, false) constraints := *createConstraintStatements(&node.Constraints, *name)
ret := header + constraints ret := header + constraints
return &ret return &ret
} }
...@@ -231,63 +201,26 @@ entities is a list of entityStructs that are needed to form the relation LET-sta ...@@ -231,63 +201,26 @@ entities is a list of entityStructs that are needed to form the relation LET-sta
Return: a string containing a single LET-statement in AQL Return: a string containing a single LET-statement in AQL
*/ */
func createRelationLetWithFromEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int) *string { func createRelationMatch(relation *entity.QueryRelationStruct, relationName string, pathName string, entities *[]entity.QueryEntityStruct, limit int, outbound bool) *string {
header := fmt.Sprintf("LET %v = (\n\tFOR x IN n%v \n", name, relation.EntityFrom) relationReturn := ""
forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v OUTBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Type) var relationBounds int
if outbound {
// Guarantees that there is no path returned with a duplicate edge relationReturn = fmt.Sprintf("MATCH %v = (n%v)-[%v:%v*%v..%v]->(", pathName, relation.EntityFrom, relationName, relation.Type, relation.Depth.Min, relation.Depth.Max)
// This way there are no cycle paths possible, TODO: more research about this needed relationBounds = relation.EntityTo
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 } else {
footer := "" relationReturn = fmt.Sprintf("MATCH %v = (n%v)-[%v:%v*%v..%v]->(", pathName, relation.EntityTo, relationName, relation.Type, relation.Depth.Min, relation.Depth.Max)
if limit != -1 { relationBounds = relation.EntityFrom
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 if relationBounds != -1 {
*/ relationReturn += fmt.Sprintf("n%v", relationBounds)
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) relationReturn += ")"
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) constraintReturn := *createConstraintStatements(&relation.Constraints, relationName)
// Dont use a limit on quantifing queries ret := relationReturn + "\n" + constraintReturn
footer := ""
if limit != -1 {
footer += fmt.Sprintf("\tLIMIT %v \n", limit)
}
footer += "RETURN DISTINCT p )\n"
ret := header + forStatement + optionStmtn + relationFilterStmnt + footer
return &ret return &ret
} }
...@@ -13,7 +13,7 @@ isRelation is a boolean specifying if this constraint comes from a node or relat ...@@ -13,7 +13,7 @@ isRelation is a boolean specifying if this constraint comes from a node or relat
Return: a string containing a FILTER-statement with all the constraints Return: a string containing a FILTER-statement with all the constraints
*/ */
func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, name string, isRelation bool) *string { func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, name string) *string {
s := "" s := ""
if len(*constraints) == 0 { if len(*constraints) == 0 {
return &s return &s
...@@ -22,7 +22,7 @@ func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, nam ...@@ -22,7 +22,7 @@ func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, nam
newLineStatement := "\tWHERE" newLineStatement := "\tWHERE"
for _, v := range *constraints { for _, v := range *constraints {
s += fmt.Sprintf("%v %v \n", newLineStatement, *createConstraintBoolExpression(&v, name, isRelation)) s += fmt.Sprintf("%v%v \n", newLineStatement, *createConstraintBoolExpression(&v, name))
newLineStatement = "\tAND" newLineStatement = "\tAND"
} }
...@@ -38,7 +38,7 @@ isRelation is a boolean specifying if this constraint comes from a node or relat ...@@ -38,7 +38,7 @@ isRelation is a boolean specifying if this constraint comes from a node or relat
Return: a string containing an boolean expression of a single constraint Return: a string containing an boolean expression of a single constraint
*/ */
func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, name string, isRelation bool) *string { func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, name string) *string {
var ( var (
match string match string
value string value string
...@@ -95,10 +95,7 @@ func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, na ...@@ -95,10 +95,7 @@ func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, na
} }
} }
if isRelation { line = fmt.Sprintf("%s %s.%s %s %s", neq, name, constraint.Attribute, match, value)
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 return &line
} }
...@@ -14,11 +14,38 @@ func main() { ...@@ -14,11 +14,38 @@ func main() {
js := []byte(`{ js := []byte(`{
"return": { "return": {
"entities": [ "entities": [
0 0,
1,
2
], ],
"relations": [] "relations": [
0,
1
]
}, },
"entities": [ "entities": [
{
"type": "airports",
"constraints": [
{
"attribute": "city",
"value": "New York",
"dataType": "text",
"matchType": "exact"
}
]
},
{
"type": "airports",
"constraints": [
{
"attribute": "city",
"value": "San Francisco",
"dataType": "text",
"matchType": "exact"
}
]
},
{ {
"type": "airports", "type": "airports",
"constraints": [ "constraints": [
...@@ -31,9 +58,37 @@ func main() { ...@@ -31,9 +58,37 @@ func main() {
] ]
} }
], ],
"relations": [], "relations": [
{
"type": "flights",
"depth": {
"min": 1,
"max": 3
},
"entityFrom": 2,
"entityTo": 1,
"constraints": [
{
"attribute": "Day",
"value": "15",
"dataType": "number",
"matchType": "EQ"
}
]
},
{
"type": "flights",
"depth": {
"min": 1,
"max": 1
},
"entityFrom": 0,
"entityTo": -1,
"constraints": []
}
],
"limit": 5000 "limit": 5000
}`) }`)
var inc entity.IncomingQueryJSON var inc entity.IncomingQueryJSON
json.Unmarshal(js, &inc) json.Unmarshal(js, &inc)
......
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