diff --git a/aql/convertQuery.go b/aql/convertQuery.go index c9adaa57559e9b09bf9abefd4838c482fcb195b8..b9b8129e10e6e019e80fa8f95148de3f88cf2c56 100644 --- a/aql/convertQuery.go +++ b/aql/convertQuery.go @@ -6,8 +6,11 @@ This program has been developed by students from the bachelor Computer Science a package aql import ( + //"encoding/json" + "encoding/json" "errors" "fmt" + "strconv" "git.science.uu.nl/graphpolaris/query-conversion/entity" ) @@ -20,36 +23,54 @@ ConvertQuery converts an IncomingQueryJSON object into AQL Returns: *string, the AQL query and a possible error */ func (s *Service) ConvertQuery(JSONQuery *entity.IncomingQueryJSON) (*string, error) { - + // TODO: MICHAEL WANT A SINGLE ENTITY TO RETURN A SUMMARY OF ATTRIBUTE VALUES (HISTOGRAM THINGIES) // Check to make sure all indexes exist // The largest possible id for an entity - largestEntityID := len(JSONQuery.Entities) - 1 + entityCount := len(JSONQuery.Entities) - 1 // The largest possible id for a relation - largestRelationID := len(JSONQuery.Relations) - 1 + relationCount := len(JSONQuery.Relations) - 1 + // There are no entities or relations, our query is empty + if entityCount < 0 && relationCount < 0 { + fmt.Println("Empty query sent, returning default response") + return defaultReturn() + } + // There are no enities or there are no relations + if entityCount < 0 || relationCount < 0 { + fmt.Println("No relations or entities sent, returning default response") + return defaultReturn() + } - // 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") + potentialErrors := entity.ValidateStruct(*JSONQuery) + // If we find the JSONQuery to be invalid we return a error + if len(potentialErrors) != 0 { + for _, err := range potentialErrors { + fmt.Printf("err: %v\n", err) } + return nil, errors.New("JSONQuery invalid") } - // 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") - } + entityMap, relationMap, _ := fixIndices(JSONQuery) + + var tree []entity.Tree + var topNode entity.QueryEntityStruct + if len(JSONQuery.Entities) != 0 && len(JSONQuery.Relations) != 0 { + tree, topNode = createHierarchy(JSONQuery, entityMap, relationMap) } - // 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") + for i, treeElement := range tree { + fmt.Println("I am triple(from,rel,to): " + strconv.Itoa(treeElement.Self.FromNode.ID) + "," + strconv.Itoa(treeElement.Self.Rel.ID) + "," + strconv.Itoa(treeElement.Self.ToNode.ID)) + fmt.Println("My relation contains the following nodes(from,to): " + strconv.Itoa(treeElement.Self.Rel.FromID) + "," + strconv.Itoa(treeElement.Self.Rel.ToID)) + fmt.Println("My index is: " + strconv.Itoa(i)) + fmt.Println("My parent index is: " + strconv.Itoa(treeElement.Parent)) + fmt.Println("My children's indices are: ") + for j := range treeElement.Children { + fmt.Println(treeElement.Children[j]) } + fmt.Println("Next please!") } - - result := createQuery(JSONQuery) + fuckshitprinter, _ := json.Marshal(tree) + fmt.Println(string(fuckshitprinter)) + result := createQuery(JSONQuery, tree, topNode, entityMap, relationMap) return result, nil } @@ -58,281 +79,227 @@ 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 +func createQuery(JSONQuery *entity.IncomingQueryJSON, tree []entity.Tree, topNode entity.QueryEntityStruct, entityMap map[int]int, relationMap map[int]int) *string { + modName := "" 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 + modifier := JSONQuery.Modifiers[0] + fmt.Println(modifier.SelectedType + ", " + strconv.Itoa(modifier.SelectedTypeID)) + if modifier.SelectedType == "entity" && modifier.SelectedTypeID == topNode.ID { + modName = fmt.Sprintf("e_%v", topNode.ID) } } - - // Include all types that are not yet included - first := true - for k := range allTypes { - if !includedTypes[k] { - if first { - ret += fmt.Sprintf("WITH %v", k) - first = false + output := createLetFor("result", fmt.Sprintf("e_%v", topNode.ID), topNode.Name, 0) + for constraint := range topNode.Constraints { + output += createFilter(topNode.Constraints[constraint], fmt.Sprintf("e_%v", topNode.ID)) + } + subQuery, subName := createQueryRecurse(JSONQuery, tree, 0, topNode, 1) + subNames := []string{subName} + output += subQuery + output += createZeroFilter(append(subNames, fmt.Sprintf("e_%v", topNode.ID))) + output += createReturn(fmt.Sprintf("e_%v", topNode.ID), "", modName, subNames) + output += ")\n" + //output += "LET nodes = union_distinct(flatten(result[**].nodes),[])\nLET edges = union_distinct(flatten(result[**].rel),[])\nLET modif = union_distinct(flatten(result[**].mod),[])\n" + if len(JSONQuery.Modifiers) == 0 { + output += "LET nodes = union_distinct(flatten(result[**].nodes),[])\nLET edges = union_distinct(flatten(result[**].rel),[])\nRETURN {\"vertices\":nodes,\"edges\":edges}" + } else { + output += "LET modif = union_distinct(flatten(result[**].mod),[])\n" + modifier := JSONQuery.Modifiers[0] + if modifier.AttributeIndex == -1 { + output += "RETURN " + getModifierType(modifier) + "(modif)" + } else { + var attribute string + // Selecting the right attribute from either the entity constraint or relation constraint + if modifier.SelectedType == "entity" { + attribute = JSONQuery.Entities[entityMap[modifier.SelectedTypeID]].Constraints[modifier.AttributeIndex].Attribute } else { - ret += fmt.Sprintf(", %v", k) + attribute = JSONQuery.Relations[relationMap[modifier.SelectedTypeID]].Constraints[modifier.AttributeIndex].Attribute } + output += "RETURN " + getModifierType(modifier) + "(modif[**]." + attribute + ")" } } - if !first { - ret += "\n" - } - - 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) + return &output +} - 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 +func createQueryRecurse(JSONQuery *entity.IncomingQueryJSON, tree []entity.Tree, currentindex int, topNode entity.QueryEntityStruct, indentindex int) (string, string) { + currentTree := tree[currentindex] + newNode := getTreeNewNode(currentTree, tree, topNode) + modName := "" + if len(JSONQuery.Modifiers) > 0 { + modifier := JSONQuery.Modifiers[0] + if modifier.SelectedType == "entity" && modifier.SelectedTypeID == newNode.ID { + modName = fmt.Sprintf("e_%v", newNode.ID) + } else if modifier.SelectedType == "relation" && modifier.SelectedTypeID == currentTree.Self.Rel.ID { + modName = fmt.Sprintf("r%v", currentTree.Self.Rel.ID) } - - // 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 + output := "" + output += fixIndent(createLetFor(fmt.Sprintf("e%v", newNode.ID), fmt.Sprintf("e_%v", newNode.ID), newNode.Name, indentindex), indentindex) + output += fixIndent(fmt.Sprintf("\tFOR r%v IN %v\n", currentTree.Self.Rel.ID, currentTree.Self.Rel.Name), indentindex) + for constraint := range newNode.Constraints { + output += fixIndent(createFilter(newNode.Constraints[constraint], fmt.Sprintf("e_%v", newNode.ID)), indentindex) } - - // 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) - } + for constraint := range currentTree.Self.Rel.Constraints { + output += fixIndent(createFilter(currentTree.Self.Rel.Constraints[constraint], fmt.Sprintf("r%v", currentTree.Self.Rel.ID)), indentindex) } + output += fixIndent(fmt.Sprintf("\tFILTER r%v._from == e_%v._id AND r%v._to == e_%v._id\n", currentTree.Self.Rel.ID, currentTree.Self.FromNode.ID, currentTree.Self.Rel.ID, currentTree.Self.ToNode.ID), indentindex) + var subNames []string + for i := range currentTree.Children { + subQuery, subName := createQueryRecurse(JSONQuery, tree, currentTree.Children[i], topNode, indentindex+1) + output += subQuery + subNames = append(subNames, subName) + } + output += fixIndent(createZeroFilter(append(subNames, fmt.Sprintf("e_%v", newNode.ID), fmt.Sprintf("r%v", currentTree.Self.Rel.ID))), indentindex) + output += fixIndent(createReturn(fmt.Sprintf("e_%v", newNode.ID), fmt.Sprintf("r%v", currentTree.Self.Rel.ID), modName, subNames), indentindex) + output += fixIndent(")\n", indentindex) + return output, fmt.Sprintf("e%v", newNode.ID) +} - //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]" - - } else { - // 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) - - } else { - 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) - - } - +func getTreeNewNode(currentTree entity.Tree, tree []entity.Tree, topNode entity.QueryEntityStruct) entity.QueryEntityStruct { + if currentTree.Parent < 0 { + if currentTree.Self.FromNode.ID == topNode.ID { + return currentTree.Self.ToNode } 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 - - } else { - 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) - - } else { - ret += fmt.Sprintf("RETURN %v (n%v[*].%v)", modifier.Type, modifier.SelectedTypeID, attribute) - - } - } + return currentTree.Self.FromNode } - + } else if currentTree.Self.FromNode.ID == tree[currentTree.Parent].Self.FromNode.ID || currentTree.Self.FromNode.ID == tree[currentTree.Parent].Self.ToNode.ID { + return currentTree.Self.ToNode } else { + return currentTree.Self.FromNode + } +} - // 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) - } +func createLetFor(variableName string, forName string, enumerableName string, indentindex int) string { + output := "LET " + variableName + " = (\n" + for i := 0; i < indentindex; i++ { + output += "\t" + } + output += "\tFOR " + forName + " IN " + enumerableName + "\n" + return output +} - for _, node := range nodesToReturn { - nodeUnion += fmt.Sprintf("%v,", node) - } - nodeUnion += "[],[]))\n" +func createFilter(constraint entity.QueryConstraintStruct, filtered string) string { + output := "\tFILTER " + filtered + "." + constraint.Attribute + " " + wordsToLogicalSign(constraint) + if constraint.DataType == "string" { + output += " \"" + } else { + output += " " + } + if constraint.MatchType == "contains" { + output += "%" + } + output += constraint.Value + if constraint.MatchType == "contains" { + output += "%" + } + if constraint.DataType == "string" { + output += "\" " + } else { + output += " " + } + output += " \n" + return output +} - relationUnion = "LET edges = first(RETURN UNION_DISTINCT(" - for _, relation := range relationsToReturn { - relationUnion += fmt.Sprintf("flatten(%v[**].edges), ", relation) +func createZeroFilter(subNames []string) string { + output := "\tFILTER" + for i := range subNames { + output += fmt.Sprintf(" length(%v) != 0", subNames[i]) + if i < len(subNames)-1 { + output += " AND" } - relationUnion += "[],[]))\n" - - ret += nodeUnion + relationUnion - ret += "RETURN {\"vertices\":nodes, \"edges\":edges }" - } - - return &ret + output += "\n" + return output } -/* -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 +func createReturn(nodeName string, relName string, modName string, subNames []string) string { + output := "\tRETURN {\"nodes\":union_distinct(" + for i := range subNames { + output += fmt.Sprintf("flatten(%v[**].nodes), ", subNames[i]) + } + output += "[" + nodeName + "]" + if len(subNames) == 0 { + output += ", []" + } + output += "), \"rel\": union_distinct(" + for i := range subNames { + output += fmt.Sprintf("flatten(%v[**].rel), ", subNames[i]) + } + output += "[" + relName + "]" + if len(subNames) == 0 { + output += ", []" + } + output += "), \"mod\": union_distinct(" + for i := range subNames { + output += fmt.Sprintf("flatten(%v[**].mod), ", subNames[i]) + } + output += "[" + modName + "]" + if len(subNames) == 0 { + output += ", []" + } + output += ")}\n" + return output } -/* -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) +func wordsToLogicalSign(element entity.QueryConstraintStruct) string { + var match string + switch element.DataType { + case "string": + switch element.MatchType { + case "NEQ": + match = "!=" + case "contains": + match = "LIKE" + case "excludes": + match = "NOT LIKE" + default: //EQ + match = "==" + } + case "int": + switch element.MatchType { + case "NEQ": + match = "!=" + case "GT": + match = ">" + case "LT": + match = "<" + case "GET": + match = ">=" + case "LET": + match = "<=" + default: //EQ + match = "==" + } + default: /*bool*/ + switch element.MatchType { + case "NEQ": + match = "!=" + default: //EQ + match = "==" + } } + return match +} - relationFilterStmnt := *createConstraintStatements(&relation.Constraints, "p", true) - - // Dont use a limit on quantifing queries - footer := "" - if limit != -1 { - footer += fmt.Sprintf("\tLIMIT %v \n", limit) +func fixIndent(input string, indentCount int) string { + output := "" + for i := 0; i < indentCount; i++ { + output += "\t" } - footer += "RETURN DISTINCT p )\n" - - ret := header + forStatement + optionStmtn + vFilterStmnt + relationFilterStmnt + footer - return &ret + output += input + return output } -/* -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) +func getModifierType(modifier entity.QueryModifierStruct) string { + if modifier.Type == "COUNT" { + return "LENGTH" } - footer += "RETURN DISTINCT p )\n" + return modifier.Type +} - ret := header + forStatement + optionStmtn + relationFilterStmnt + footer - return &ret +func defaultReturn() (*string, error) { + defaultReturn := `LET nodes = first(RETURN UNION_DISTINCT([],[])) + LET edges = first(RETURN UNION_DISTINCT([],[])) + RETURN {"vertices":nodes, "edges":edges }` + return &defaultReturn, nil } diff --git a/aql/convertQuery_test.go b/aql/convertQuery_test.go index 25b4358aa14054ed7b88ec968faea3b562b09e07..5effaf767a7baae2f07d03b04bf8ab2cfa3cd195 100644 --- a/aql/convertQuery_test.go +++ b/aql/convertQuery_test.go @@ -8,7 +8,7 @@ package aql import ( "encoding/json" "errors" - "strings" + "regexp" "testing" "git.science.uu.nl/graphpolaris/query-conversion/entity" @@ -31,6 +31,8 @@ func TestEmptyQueryConversion(t *testing.T) { }, "entities": [], "relations": [], + "groupBys": [], + "filters": [], "limit": 5000 }`) @@ -44,72 +46,67 @@ func TestEmptyQueryConversion(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := ` -LET nodes = first(RETURN UNION_DISTINCT([],[])) + correctConvertedResult := `LET nodes = first(RETURN UNION_DISTINCT([],[])) LET edges = first(RETURN UNION_DISTINCT([],[])) RETURN {"vertices":nodes, "edges":edges }` - assert.Equal(t, correctConvertedResult, *convertedResult) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* -Tests multiple entity types +Tests two entities (two types) without a filter +Query description: Give me all parties connected to their respective parliament members t: *testing.T, makes go recognise this as a test */ -func TestMultipleEntityTypes(t *testing.T) { +func TestTwoEntitiesNoFilter(t *testing.T) { // Setup for test // Create query conversion service service := NewService() query := []byte(`{ - "databaseName": "test", + "databaseName": "TweedeKamer", "return": { - "entities": [ - 0, - 1 - ], - "relations": [ - 0 - ] + "entities": [ + 0, + 1 + ], + "relations": [ + 0 + ] }, "entities": [ - { - "type": "kamerleden", - "constraints": [ - { - "attribute": "partij", - "value": "GL", - "dataType": "text", - "matchType": "exact" - } - ] - }, - { - "type": "partijen", - "constraints": [ - { - "attribute": "zetels", - "value": "6", - "dataType": "number", - "matchType": "GT" - } - ] - } + { + "name": "parliament", + "ID": 0, + "constraints": [] + }, + { + "name": "parties", + "ID": 1, + "constraints": [] + } ], "relations": [ - { - "type": "lid_van", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": 0, - "entityTo": 1, - "constraints": [] - } + { + "ID": 0, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 1, + "constraints":[] + } ], + "groupBys": [], "limit": 5000, "modifiers": [] - }`) + }`) // Unmarshall the incoming message into an IncomingJSONQuery object var JSONQuery entity.IncomingQueryJSON @@ -121,46 +118,86 @@ func TestMultipleEntityTypes(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := "WITH partijen\nLET n0 = (\n\tFOR x IN kamerleden \n\tFILTER x.partij == \"GL\" \n\tRETURN x\n)\nLET r0 = (\n\tFOR x IN n0 \n\tFOR v, e, p IN 1..1 OUTBOUND x lid_van \n\tOPTIONS { uniqueEdges: \"path\" }\n\tFILTER v.zetels > 6 \n\tLIMIT 5000 \nRETURN DISTINCT p )\n\nLET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))\nLET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))\nRETURN {\"vertices\":nodes, \"edges\":edges }" - cleanedResult := strings.ReplaceAll(correctConvertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - convertedCleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - convertedCleanedResult = strings.ReplaceAll(convertedCleanedResult, "\t", "") - - assert.Equal(t, convertedCleanedResult, cleanedResult) + correctConvertedResult := []byte(`LET result = ( + FOR e_0 IN parliament + LET e1 = ( + FOR e_1 IN parties + FOR r0 IN member_of + FILTER r0._from == e_0._id AND r0._to == e_1._id + FILTER length(e_1) != 0 AND length(r0) != 0 + RETURN {"nodes":union_distinct([e_1], []), "rel": union_distinct([r0], []), "mod": union_distinct([], [])} + ) + FILTER length(e1) != 0 AND length(e_0) != 0 + RETURN {"nodes":union_distinct(flatten(e1[**].nodes), [e_0]), "rel": union_distinct(flatten(e1[**].rel), []), "mod": union_distinct(flatten(e1[**].mod), [])} +) +LET nodes = union_distinct(flatten(result[**].nodes),[]) +LET edges = union_distinct(flatten(result[**].rel),[]) +RETURN {"vertices":nodes,"edges":edges}`) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* -Tests a query with one attribute +Tests two entities (two types) with one entity filter +Query description: Give me all parties, with less than 10 seats, connected to their respective parliament members t: *testing.T, makes go recognise this as a test */ -func TestEntityOneAttributeQuery(t *testing.T) { +func TestTwoEntitiesOneEntityFilter(t *testing.T) { // Setup for test // Create query conversion service service := NewService() query := []byte(`{ + "databaseName": "TweedeKamer", "return": { "entities": [ - 0 + 0, + 1 ], - "relations": [] + "relations": [ + 0 + ] }, "entities": [ { - "type": "airports", + "name": "parliament", + "ID": 0, + "constraints": [] + }, + { + "name": "parties", + "ID": 1, "constraints": [ { - "attribute": "state", - "value": "HI", - "dataType": "string", - "matchType": "exact" + "attribute": "seats", + "value": "10", + "dataType": "int", + "matchType": "LT" } ] } ], - "relations": [], - "limit": 5000 + "relations": [ + { + "ID": 0, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 1, + "constraints":[] + } + ], + "groupBys": [], + "limit": 5000, + "modifiers": [] }`) // Unmarshall the incoming message into an IncomingJSONQuery object @@ -173,25 +210,44 @@ func TestEntityOneAttributeQuery(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET nodes = first(RETURN UNION_DISTINCT(n0,[],[]))LET edges = first(RETURN UNION_DISTINCT([],[]))RETURN {"vertices":nodes, "edges":edges }` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) + correctConvertedResult := []byte(`LET result = ( + FOR e_0 IN parliament + LET e1 = ( + FOR e_1 IN parties + FOR r0 IN member_of + FILTER e_1.seats < 10 + FILTER r0._from == e_0._id AND r0._to == e_1._id + FILTER length(e_1) != 0 AND length(r0) != 0 + RETURN {"nodes":union_distinct([e_1], []), "rel": union_distinct([r0], []), "mod": union_distinct([], [])} + ) + FILTER length(e1) != 0 AND length(e_0) != 0 + RETURN {"nodes":union_distinct(flatten(e1[**].nodes), [e_0]), "rel": union_distinct(flatten(e1[**].rel), []), "mod": union_distinct(flatten(e1[**].mod), [])} +) +LET nodes = union_distinct(flatten(result[**].nodes),[]) +LET edges = union_distinct(flatten(result[**].rel),[]) +RETURN {"vertices":nodes,"edges":edges}`) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* -Test a relation with a constraint +Tests two entities (two types) with two entity filters +Query description: Give me all parties, with less than 10 seats, connected to their respective parliament members, who are more than 45 years old t: *testing.T, makes go recognise this as a test */ -func TestRelationWithConstraint(t *testing.T) { +func TestTwoEntitiesTwoEntityFilters(t *testing.T) { // Setup for test // Create query conversion service service := NewService() query := []byte(`{ + "databaseName": "TweedeKamer", "return": { "entities": [ - 0 + 0, + 1 ], "relations": [ 0 @@ -199,37 +255,48 @@ func TestRelationWithConstraint(t *testing.T) { }, "entities": [ { - "type": "airports", + "name": "parliament", + "ID": 0, "constraints": [ { - "attribute": "state", - "value": "HI", - "dataType": "string", - "matchType": "exact" + "attribute": "age", + "value": "45", + "dataType": "int", + "matchType": "GT" + } + ] + }, + { + "name": "parties", + "ID": 1, + "constraints": [ + { + "attribute": "seats", + "value": "10", + "dataType": "int", + "matchType": "LT" } ] } ], "relations": [ { - "type": "flights", + "ID": 0, + "name": "member_of", "depth": { "min": 1, "max": 1 }, - "entityFrom": 0, - "entityTo": -1, - "constraints": [ - { - "attribute": "Day", - "value": "15", - "dataType": "int", - "matchType": "EQ" - } - ] + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 1, + "constraints":[] } ], - "limit": 5000 + "groupBys": [], + "limit": 5000, + "modifiers": [] }`) // Unmarshall the incoming message into an IncomingJSONQuery object @@ -242,51 +309,100 @@ func TestRelationWithConstraint(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) + correctConvertedResult := []byte(`LET result = ( + FOR e_0 IN parliament + FILTER e_0.age > 45 + LET e1 = ( + FOR e_1 IN parties + FOR r0 IN member_of + FILTER e_1.seats < 10 + FILTER r0._from == e_0._id AND r0._to == e_1._id + FILTER length(e_1) != 0 AND length(r0) != 0 + RETURN {"nodes":union_distinct([e_1], []), "rel": union_distinct([r0], []), "mod": union_distinct([], [])} + ) + FILTER length(e1) != 0 AND length(e_0) != 0 + RETURN {"nodes":union_distinct(flatten(e1[**].nodes), [e_0]), "rel": union_distinct(flatten(e1[**].rel), []), "mod": union_distinct(flatten(e1[**].mod), [])} +) +LET nodes = union_distinct(flatten(result[**].nodes),[]) +LET edges = union_distinct(flatten(result[**].rel),[]) +RETURN {"vertices":nodes,"edges":edges}`) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* -Tests the count modifier +Tests three entities (three types) without a filter +Query description: Give me all parties, connected to their respective parliament members, who are then connected to the resolutions they submitted t: *testing.T, makes go recognise this as a test */ -func TestModifierCountEntity(t *testing.T) { +func TestThreeEntitiesNoFilter(t *testing.T) { // Setup for test // Create query conversion service service := NewService() query := []byte(`{ + "databaseName": "TweedeKamer", "return": { "entities": [ - 0 + 0, + 1, + 2 ], - "relations": [] + "relations": [ + 0, + 1 + ] }, "entities": [ { - "type": "airports", - "constraints": [ - { - "attribute": "state", - "value": "HI", - "dataType": "string", - "matchType": "exact" - } - ] + "name": "parliament", + "ID": 0, + "constraints": [] + }, + { + "name": "parties", + "ID": 1, + "constraints": [] + }, + { + "name": "resolutions", + "ID": 2, + "constraints": [] } ], - "relations": [], - "limit": 5000, - "modifiers": [ + "relations": [ + { + "ID": 0, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 1, + "constraints":[] + }, { - "type": "COUNT", - "selectedType": "entity", - "id": 0, - "attributeIndex": -1 + "ID": 1, + "name": "submits", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 2, + "constraints":[] } - ] + ], + "groupBys": [], + "limit": 5000, + "modifiers": [] }`) // Unmarshall the incoming message into an IncomingJSONQuery object @@ -299,51 +415,112 @@ func TestModifierCountEntity(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)RETURN LENGTH (n0)` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) + correctConvertedResult := []byte(`LET result = ( + FOR e_1 IN parties + LET e0 = ( + FOR e_0 IN parliament + FOR r0 IN member_of + FILTER r0._from == e_0._id AND r0._to == e_1._id + LET e2 = ( + FOR e_2 IN resolutions + FOR r1 IN submits + FILTER r1._from == e_0._id AND r1._to == e_2._id + FILTER length(e_2) != 0 AND length(r1) != 0 + RETURN {"nodes":union_distinct([e_2], []), "rel": union_distinct([r1], []), "mod": union_distinct([], [])} + ) + FILTER length(e2) != 0 AND length(e_0) != 0 AND length(r0) != 0 + RETURN {"nodes":union_distinct(flatten(e2[**].nodes), [e_0]), "rel": union_distinct(flatten(e2[**].rel), [r0]), "mod": union_distinct(flatten(e2[**].mod), [])} + ) + FILTER length(e0) != 0 AND length(e_1) != 0 + RETURN {"nodes":union_distinct(flatten(e0[**].nodes), [e_1]), "rel": union_distinct(flatten(e0[**].rel), []), "mod": union_distinct(flatten(e0[**].mod), [])} +) +LET nodes = union_distinct(flatten(result[**].nodes),[]) +LET edges = union_distinct(flatten(result[**].rel),[]) +RETURN {"vertices":nodes,"edges":edges}`) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* -Tests the count modifer with an attribute +Tests three entities (three types) with one entity filter +Query description: Give me all parties, connected to their respective parliament members, whose name has "Geert" in it (this results in only "Geert Wilders"), who are/is then connected to the resolutions they submitted t: *testing.T, makes go recognise this as a test */ -func TestModifierCountEntityAttribute(t *testing.T) { +func TestThreeEntitiesOneEntityFilter(t *testing.T) { // Setup for test // Create query conversion service service := NewService() query := []byte(`{ + "databaseName": "TweedeKamer", "return": { "entities": [ - 0 + 0, + 1, + 2 ], - "relations": [] + "relations": [ + 0, + 1 + ] }, "entities": [ { - "type": "airports", + "name": "parliament", + "ID": 0, "constraints": [ { - "attribute": "state", - "value": "HI", + "attribute": "name", + "value": "Geert", "dataType": "string", - "matchType": "exact" + "matchType": "contains" } ] + }, + { + "name": "parties", + "ID": 1, + "constraints": [] + }, + { + "name": "resolutions", + "ID": 2, + "constraints": [] } ], - "relations": [], - "limit": 5000, - "modifiers": [ + "relations": [ + { + "ID": 0, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 1, + "constraints":[] + }, { - "type": "SUM", - "selectedType": "entity", - "id": 0, - "attributeIndex": 0 + "ID": 1, + "name": "submits", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 2, + "constraints":[] } - ] + ], + "groupBys": [], + "limit": 5000, + "modifiers": [] }`) // Unmarshall the incoming message into an IncomingJSONQuery object @@ -356,71 +533,120 @@ func TestModifierCountEntityAttribute(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)RETURN SUM (n0[*].state)` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) + correctConvertedResult := []byte(`LET result = ( + FOR e_1 IN parties + LET e0 = ( + FOR e_0 IN parliament + FOR r0 IN member_of + FILTER e_0.name LIKE "%Geert%" + FILTER r0._from == e_0._id AND r0._to == e_1._id + LET e2 = ( + FOR e_2 IN resolutions + FOR r1 IN submits + FILTER r1._from == e_0._id AND r1._to == e_2._id + FILTER length(e_2) != 0 AND length(r1) != 0 + RETURN {"nodes":union_distinct([e_2], []), "rel": union_distinct([r1], []), "mod": union_distinct([], [])} + ) + FILTER length(e2) != 0 AND length(e_0) != 0 AND length(r0) != 0 + RETURN {"nodes":union_distinct(flatten(e2[**].nodes), [e_0]), "rel": union_distinct(flatten(e2[**].rel), [r0]), "mod": union_distinct(flatten(e2[**].mod), [])} + ) + FILTER length(e0) != 0 AND length(e_1) != 0 + RETURN {"nodes":union_distinct(flatten(e0[**].nodes), [e_1]), "rel": union_distinct(flatten(e0[**].rel), []), "mod": union_distinct(flatten(e0[**].mod), [])} +) +LET nodes = union_distinct(flatten(result[**].nodes),[]) +LET edges = union_distinct(flatten(result[**].rel),[]) +RETURN {"vertices":nodes,"edges":edges}`) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* -Tests the count modifier on a relation +Tests three entities (three types) with two entity filters +Query description: Give me all parties, connected to their respective parliament members, whose name has "Geert" in it (this results in only "Geert Wilders"), who are/is then connected to the resolutions they submitted, but only those submitted in May t: *testing.T, makes go recognise this as a test */ -func TestModifierCountRelation(t *testing.T) { +func TestThreeEntitiesTwoEntityFilters(t *testing.T) { // Setup for test // Create query conversion service service := NewService() query := []byte(`{ + "databaseName": "TweedeKamer", "return": { "entities": [ - 0 + 0, + 1, + 2 ], "relations": [ - 0 + 0, + 1 ] }, "entities": [ { - "type": "airports", + "name": "parliament", + "ID": 0, "constraints": [ { - "attribute": "state", - "value": "HI", + "attribute": "name", + "value": "Geert", "dataType": "string", - "matchType": "exact" + "matchType": "contains" + } + ] + }, + { + "name": "parties", + "ID": 1, + "constraints": [] + }, + { + "name": "resolutions", + "ID": 2, + "constraints": [ + { + "attribute": "date", + "value": "mei", + "dataType": "string", + "matchType": "contains" } ] } ], "relations": [ { - "type": "flights", + "ID": 0, + "name": "member_of", "depth": { "min": 1, "max": 1 }, - "entityFrom": 0, - "entityTo": -1, - "constraints": [ - { - "attribute": "Day", - "value": "15", - "dataType": "int", - "matchType": "EQ" - } - ] + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 1, + "constraints":[] + }, + { + "ID": 1, + "name": "submits", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 2, + "constraints":[] } ], + "groupBys": [], "limit": 5000, - "modifiers": [ - { - "type": "COUNT", - "selectedType": "relation", - "id": 0, - "attributeIndex": -1 - } - ] + "modifiers": [] }`) // Unmarshall the incoming message into an IncomingJSONQuery object @@ -433,144 +659,225 @@ func TestModifierCountRelation(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER p.edges[*].Day ALL == 15 RETURN DISTINCT p )RETURN LENGTH (unique(r0[*].edges[**]))` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) + correctConvertedResult := []byte(`LET result = ( + FOR e_1 IN parties + LET e0 = ( + FOR e_0 IN parliament + FOR r0 IN member_of + FILTER e_0.name LIKE "%Geert%" + FILTER r0._from == e_0._id AND r0._to == e_1._id + LET e2 = ( + FOR e_2 IN resolutions + FOR r1 IN submits + FILTER e_2.date LIKE "%mei%" + FILTER r1._from == e_0._id AND r1._to == e_2._id + FILTER length(e_2) != 0 AND length(r1) != 0 + RETURN {"nodes":union_distinct([e_2], []), "rel": union_distinct([r1], []), "mod": union_distinct([], [])} + ) + FILTER length(e2) != 0 AND length(e_0) != 0 AND length(r0) != 0 + RETURN {"nodes":union_distinct(flatten(e2[**].nodes), [e_0]), "rel": union_distinct(flatten(e2[**].rel), [r0]), "mod": union_distinct(flatten(e2[**].mod), [])} + ) + FILTER length(e0) != 0 AND length(e_1) != 0 + RETURN {"nodes":union_distinct(flatten(e0[**].nodes), [e_1]), "rel": union_distinct(flatten(e0[**].rel), []), "mod": union_distinct(flatten(e0[**].mod), [])} +) +LET nodes = union_distinct(flatten(result[**].nodes),[]) +LET edges = union_distinct(flatten(result[**].rel),[]) +RETURN {"vertices":nodes,"edges":edges}`) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* -Tests the count modifier with an entity swap - t: *testing.T, makes go recognise this as a test -*/ -func TestModifierCountEntitySwap(t *testing.T) { - // Setup for test - // Create query conversion service - service := NewService() - - query := []byte(`{ - "databaseName": "TweedeKamer", - "return": { - "entities": [ - 0, - 1 - ], - "relations": [ - 0 - ] - }, - "entities": [ - { - "type": "partijen", - "constraints": [] - }, - { - "type": "kamerleden", - "constraints": [] - } - ], - "relations": [ - { - "type": "lid_van", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": 1, - "entityTo": 0, - "constraints": [] - } - ], - "limit": 5000, - "modifiers": [ - { - "type": "COUNT", - "selectedType": "entity", - "selectedTypeId": 1, - "attributeIndex": -1 - } - ] - }`) - - // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON - json.Unmarshal(query, &JSONQuery) - convertedResult, err := service.ConvertQuery(&JSONQuery) - - // Assert that there is no error - assert.NoError(t, err) - - // Assert that the result and the expected result are the same - correctConvertedResult := `WITH partijenLET n1 = (FOR x IN kamerleden RETURN x)LET r0 = (FOR x IN n1 FOR v, e, p IN 1..1 OUTBOUND x lid_van OPTIONS { uniqueEdges: "path" }RETURN DISTINCT p )RETURN LENGTH (unique(r0[*].vertices[0]))` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) -} +Tests five entities (three types) with four entity filters +Query description: Give me all parties, which have less than 10 seats, connected to their respective parliament members, whose name has "A" in it, who are/is then connected to the resolutions they submitted, but only those submitted in May, which are then also connected to all other persons who were part of that submission, who are part of the "VVD" +Translator's note: This returns a member of the PvdA who submitted a motion alongside a member of the VVD +hmmmmmmmmmmmmmmmmmmmmmmmmmmyo+/oooooooooooooooo+/+oymmmmmmmmmhso++/+++oo+++/++ohmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmmmmmmmmmmmmmmmmmho/+ooooooooooooooooooooooo+/oymdyo+/+ooooooooooooooooo++ymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmmmmmmmmmmmmmmmh++ooooooooooooooooooooooooooooo+/:+ooooooooooooooooooooooo++dmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmmmmmmmmmmmmmdo/ooooooooooooooooooooooooooooooooo//oooooooooooooooooooooooo+/dmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmmmmmmmmmmmmy/+ooooooooooooo++////////////++oooooo//ooooooooooooooooooooooooo/dmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmmmmmmmmmmmo/ooooooooooo+///++++oooooooo++++///++oo:+oooooooooooooooooooooooo++mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmmmmmmmmmd++ooooooooo+//++oooooooooooooooooooo++////:+++////////////////////++:ydmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmmmmmmmmd/+oooooooooo++ooooooooooooooooooooooooooo+/:/++ooooooooooooooooo+o+++++++oshmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmmmmmmmd/+oooooooooooooooooooooooooooooo+++++++++++++//+ooooooooooooooooooooooooooo+++ohmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmmmmmmm++oooooooooooooooooooooooo+++////++++++++++++++/://+ooooo+++++////////////////++//mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmmmmmms/oooooooooooooooooooooo++//+++++//////////////++++++:++///++/++++///////////////+/+ohmmmmNmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmmmdhs++:ooooooooooooooooooo+++//+++////+++++sosysssso+++++//://////++++++++//////+o+++++///+/smmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmmms+++o+/ooooooooooooooo+///+++++//+osyy+:+:...-:yNMMNmddyso++/++ooooooyho--o-````-+dmddyo+++//ymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmmd++oooo:+oooooooooooo+++++++////+oydNMy. `s: `:-` :mMMMMMMMMNmy/oooydNMMo` .o- .+:` -mMMMNdhhso/ommmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmmmh/oooooo:ooooooooooooo//+++++oooymMMMMN. .. :dh. +MMMMMMMMMMMoodNMMMMN` -. :hs` oMMMMMMMMNyommmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmdy/ooooooo/ooooooooooooo+//////:yNMMMMMMm` :- ` :MMMMMMMMMMNsMMMMMMMd -. /MMMMMMMMMN+mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hmy/ooooooooooooooooooooooooooooo++smNMMMMN. `sMMMMMMMMMNsmMMMMMMMm. .hMMMMMMNmhshmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +hh/ooooooooooooooooooooooooooooooo+o++oymNNd:` `:hMMMMMNNmyo+:sdmNNNNMMd:`` `+dNmmhysoo++mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +y/oooooooooooooooooooooooooooooooo///+o+++osys+/://+shdddysoo+++oo+:++++++ooosso/-----/++/++++o++oydmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +-+ooooooooooooooooooooooooooooooooooo+////+++++++////++++oooo+/////ooooooooooooooooooooooooooo+ommmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/ooooooooooooooooooooooooooooooooooooooooo++/////////////////+oooooooooooooooooooooooooooooo++ymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/ooooooooooooooooooooooooooooooooooooooooooooooooooooooo+/+//ooooooooo+/+oooooooooooooo+/ossdmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/oooooooooooooooooooooooooooooooooooooooooooooooooo++///+oooooooooooooo+////:////////////ymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/oooooooooooooooooooooooooooooooooooooooooooooo+///+oooooooooooooooooooooooo+/+oooooooooo++ymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo++dmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo+/dmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo+/mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo/ymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/ooooooooooooooooooooooooooooooo++++++++++ooooooooooooooooooooooooooooooooooooooooooooooooooooo/mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/oooooooooooooooooooooooooooo+//++++++++///////++++++ooooooooooooooooooooooooooooooooooooo+++///+smmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/oooooooooooooooooooooooooo+/++oooooooooooooo++++++////////+++++++oooooooooooooooo+++++////+++oo+:mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/ooooooooooooooooooooooooo+:+oooo+///////++++++ooooooooo++++++++///////////////////++++++ooooo+++ymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/ooooooooooooooooooooooooo:+ooooo++++++++++++//////++++++++ooooooooooooooooooooooooooooo++++++oydmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/ooooooooooooooooooooooooo+/++++oooooooooooooooo++++++++///////////+/++++++++++////////////+:smmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/ooooooooooooooooooooo/+ooo+///////////////+++++++oooooooooooo+++++++++++++++++++++++ooooooo+/mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +/+ooooooooooooooooooooo//ooooooooooooooooo++++++/////++++++++++++++oooooooooooooooooooooo++++hmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +y++ooooooooooooooooooooo+//+++ooooooooooooooooooooooo++++////////////////////////++//++++osymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +yd//++ooooooooooooooooooooo+++oooooooooooooooooooooooooooooooooooooooooooo++++++++//sddmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +::::////++oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo+++ohmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +:/:::://+/////+oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo+++oymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +://////::://+++///////+++ooooooooooooooooooooooooooooooooooooooooooooo++++sydmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +://////////::::/++ooo+++////////////++++++ooooooooooooooooo+++++/////shdmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +-///////////////::::://+++oooooooooo+++++////////////////////+++/::::odmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm -/* -Tests the count modifier on a relation and attribute t: *testing.T, makes go recognise this as a test */ -func TestModifierCountRelationAttribute(t *testing.T) { +func TestFiveEntitiesFourEntityFilters(t *testing.T) { // Setup for test // Create query conversion service service := NewService() query := []byte(`{ + "databaseName": "TweedeKamer", "return": { "entities": [ - 0 + 0, + 1, + 2, + 3, + 4 ], "relations": [ - 0 + 0, + 1, + 2, + 3 ] }, "entities": [ { - "type": "airports", + "name": "parliament", + "ID": 0, "constraints": [ { - "attribute": "state", - "value": "HI", + "attribute": "name", + "value": "A", "dataType": "string", - "matchType": "exact" + "matchType": "contains" } ] - } - ], - "relations": [ + }, { - "type": "flights", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": 0, - "entityTo": -1, + "name": "parties", + "ID": 1, "constraints": [ { - "attribute": "Day", - "value": "15", + "attribute": "seats", + "value": "10", "dataType": "int", - "matchType": "EQ" + "matchType": "LT" } ] - } - ], - "limit": 5000, - "modifiers": [ + }, { - "type": "AVG", - "selectedType": "relation", - "id": 0, - "attributeIndex": 0 - } - ] - }`) - - // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON - json.Unmarshal(query, &JSONQuery) + "name": "resolutions", + "ID": 2, + "constraints": [ + { + "attribute": "date", + "value": "mei", + "dataType": "string", + "matchType": "contains" + } + ] + }, + { + "name": "parliament", + "ID": 3, + "constraints":[] + }, + { + "name": "parties", + "ID": 4, + "constraints": [ + { + "attribute": "name", + "value": "Volkspartij voor Vrijheid en Democratie", + "dataType": "string", + "matchType": "==" + } + ] + } + ], + "relations": [ + { + "ID": 0, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 1, + "constraints": [] + }, + { + "ID": 1, + "name": "submits", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 2, + "constraints": [] + }, + { + "ID": 2, + "name": "submits", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 3, + "toType": "entity", + "toID": 2, + "constraints": [] + }, + { + "ID": 3, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 3, + "toType": "entity", + "toID": 4, + "constraints": [] + } + ], + "groupBys": [], + "limit": 5000, + "modifiers": [] + }`) + + // Unmarshall the incoming message into an IncomingJSONQuery object + var JSONQuery entity.IncomingQueryJSON + json.Unmarshal(query, &JSONQuery) convertedResult, err := service.ConvertQuery(&JSONQuery) @@ -578,17 +885,58 @@ func TestModifierCountRelationAttribute(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER p.edges[*].Day ALL == 15 RETURN DISTINCT p )RETURN AVG (r0[*].edges[**].Day)` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) + correctConvertedResult := []byte(`LET result = ( + FOR e_1 IN parties + FILTER e_1.seats < 10 + LET e0 = ( + FOR e_0 IN parliament + FOR r0 IN member_of + FILTER e_0.name LIKE "%A%" + FILTER r0._from == e_0._id AND r0._to == e_1._id + LET e2 = ( + FOR e_2 IN resolutions + FOR r1 IN submits + FILTER e_2.date LIKE "%mei%" + FILTER r1._from == e_0._id AND r1._to == e_2._id + LET e3 = ( + FOR e_3 IN parliament + FOR r2 IN submits + FILTER r2._from == e_3._id AND r2._to == e_2._id + LET e4 = ( + FOR e_4 IN parties + FOR r3 IN member_of + FILTER e_4.name == "Volkspartij voor Vrijheid en Democratie" + FILTER r3._from == e_3._id AND r3._to == e_4._id + FILTER length(e_4) != 0 AND length(r3) != 0 + RETURN {"nodes":union_distinct([e_4], []), "rel": union_distinct([r3], []), "mod": union_distinct([], [])} + ) + FILTER length(e4) != 0 AND length(e_3) != 0 AND length(r2) != 0 + RETURN {"nodes":union_distinct(flatten(e4[**].nodes), [e_3]), "rel": union_distinct(flatten(e4[**].rel), [r2]), "mod": union_distinct(flatten(e4[**].mod), [])} + ) + FILTER length(e3) != 0 AND length(e_2) != 0 AND length(r1) != 0 + RETURN {"nodes":union_distinct(flatten(e3[**].nodes), [e_2]), "rel": union_distinct(flatten(e3[**].rel), [r1]), "mod": union_distinct(flatten(e3[**].mod), [])} + ) + FILTER length(e2) != 0 AND length(e_0) != 0 AND length(r0) != 0 + RETURN {"nodes":union_distinct(flatten(e2[**].nodes), [e_0]), "rel": union_distinct(flatten(e2[**].rel), [r0]), "mod": union_distinct(flatten(e2[**].mod), [])} + ) + FILTER length(e0) != 0 AND length(e_1) != 0 + RETURN {"nodes":union_distinct(flatten(e0[**].nodes), [e_1]), "rel": union_distinct(flatten(e0[**].rel), []), "mod": union_distinct(flatten(e0[**].mod), [])} +) +LET nodes = union_distinct(flatten(result[**].nodes),[]) +LET edges = union_distinct(flatten(result[**].rel),[]) +RETURN {"vertices":nodes,"edges":edges}`) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* -Tests a relation with an in out constraint +Tests five entities (four types) with three entity filters and one junction +Query description: Give me all parties, with less than 10 seats, connected to their respective parliament members, who are then connected to the resolutions they submitted, but only those submitted in May, and connected to the comissions they're in, which are then connected to all of their members, but only those with "Geert" in their name (resulting in only "Geert Wilders") t: *testing.T, makes go recognise this as a test */ -func TestRelationWithInOutConstraint(t *testing.T) { +func TestSingleJunctionFiveEntitiesThreeEntityFilters(t *testing.T) { // Setup for test // Create query conversion service service := NewService() @@ -597,53 +945,120 @@ func TestRelationWithInOutConstraint(t *testing.T) { "return": { "entities": [ 0, - 1 + 1, + 2, + 3, + 4 ], "relations": [ - 0 + 0, + 1, + 2, + 3 ] }, "entities": [ { - "type": "airports", + "name": "parliament", + "ID": 0, "constraints": [ { - "attribute": "city", - "value": "San Francisco", + "attribute": "name", + "value": "Geert", "dataType": "string", - "matchType": "exact" + "matchType": "contains" } ] }, { - "type": "airports", + "name": "commissions", + "ID": 1, + "constraints": [] + }, + { + "name": "parliament", + "ID": 2, + "constraints": [] + }, + { + "name": "parties", + "ID": 3, "constraints": [ { - "attribute": "state", - "value": "HI", + "attribute": "seats", + "value": "10", + "dataType": "int", + "matchType": "LT" + } + ] + }, + { + "name": "resolutions", + "ID": 4, + "constraints": [ + { + "attribute": "date", + "value": "mei", "dataType": "string", - "matchType": "exact" + "matchType": "contains" } ] } + ], + "groupBys": [], "relations": [ { - "type": "flights", + "ID": 0, + "name": "part_of", "depth": { "min": 1, - "max": 3 + "max": 1 }, - "entityFrom": 1, - "entityTo": 0, - "constraints": [ - { - "attribute": "Day", - "value": "15", - "dataType": "int", - "matchType": "EQ" - } - ] + "fromType": "entity", + "fromId": 0, + "toType": "entity", + "toID": 1, + "constraints": [] + }, + { + "ID": 1, + "name": "part_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 2, + "toType": "entity", + "toID": 1, + "constraints": [] + }, + { + "ID": 2, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 2, + "toType": "entity", + "toID": 3, + "constraints": [] + }, + { + "ID": 3, + "name": "submits", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 2, + "toType": "entity", + "toID": 4, + "constraints": [] } ], "limit": 5000 @@ -659,17 +1074,57 @@ func TestRelationWithInOutConstraint(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `LET n1 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n1 FOR v, e, p IN 1..3 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER v.city == "San Francisco" FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) + correctConvertedResult := []byte(`LET result = ( + FOR e_0 IN parliament + FILTER e_0.name LIKE "%Geert%" + LET e1 = ( + FOR e_1 IN commissions + FOR r0 IN part_of + FILTER r0._from == e_0._id AND r0._to == e_1._id + LET e2 = ( + FOR e_2 IN parliament + FOR r1 IN part_of + FILTER r1._from == e_2._id AND r1._to == e_1._id + LET e3 = ( + FOR e_3 IN parties + FOR r2 IN member_of + FILTER e_3.seats < 10 + FILTER r2._from == e_2._id AND r2._to == e_3._id + FILTER length(e_3) != 0 AND length(r2) != 0 + RETURN {"nodes":union_distinct([e_3], []), "rel": union_distinct([r2], []), "mod": union_distinct([], [])} + ) + LET e4 = ( + FOR e_4 IN resolutions + FOR r3 IN submits + FILTER e_4.date LIKE "%mei%" + FILTER r3._from == e_2._id AND r3._to == e_4._id + FILTER length(e_4) != 0 AND length(r3) != 0 + RETURN {"nodes":union_distinct([e_4], []), "rel": union_distinct([r3], []), "mod": union_distinct([], [])} + ) + FILTER length(e3) != 0 AND length(e4) != 0 AND length(e_2) != 0 AND length(r1) != 0 + RETURN {"nodes":union_distinct(flatten(e3[**].nodes), flatten(e4[**].nodes), [e_2]), "rel": union_distinct(flatten(e3[**].rel), flatten(e4[**].rel), [r1]), "mod": union_distinct(flatten(e3[**].mod), flatten(e4[**].mod), [])} + ) + FILTER length(e2) != 0 AND length(e_1) != 0 AND length(r0) != 0 + RETURN {"nodes":union_distinct(flatten(e2[**].nodes), [e_1]), "rel": union_distinct(flatten(e2[**].rel), [r0]), "mod": union_distinct(flatten(e2[**].mod), [])} + ) + FILTER length(e1) != 0 AND length(e_0) != 0 + RETURN {"nodes":union_distinct(flatten(e1[**].nodes), [e_0]), "rel": union_distinct(flatten(e1[**].rel), []), "mod": union_distinct(flatten(e1[**].mod), [])} +) +LET nodes = union_distinct(flatten(result[**].nodes),[]) +LET edges = union_distinct(flatten(result[**].rel),[]) +RETURN {"vertices":nodes,"edges":edges}`) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* -Tests two relations +Tests nine entities (four types) with three entity filters and two junctions +Query description: Give me all parties, with less than 10 seats, connected to their respective parliament members, who are then connected to the resolutions they submitted, but only those submitted in May, and connected to the comissions they're in, which are then connected to all of their members, but only those with "Geert" in their name (resulting in only "Geert Wilders"), who is then connected to their submited resolutions and their party, which is connected to all of its members t: *testing.T, makes go recognise this as a test */ -func TestTwoRelations(t *testing.T) { +func TestDoubleJunctionNineEntitiesThreeEntityFilters(t *testing.T) { // Setup for test // Create query conversion service service := NewService() @@ -679,74 +1134,180 @@ func TestTwoRelations(t *testing.T) { "entities": [ 0, 1, - 2 + 2, + 3, + 4, + 5, + 6, + 7 ], "relations": [ 0, - 1 + 1, + 2, + 3, + 4, + 5, + 6 ] }, "entities": [ { - "type": "airports", + "name": "parliament", + "ID": 0, "constraints": [ { - "attribute": "city", - "value": "New York", + "attribute": "name", + "value": "Geert", "dataType": "string", - "matchType": "exact" + "matchType": "contains" } ] }, { - "type": "airports", + "name": "commissions", + "ID": 1, + "constraints": [] + }, + { + "name": "parliament", + "ID": 2, + "constraints": [] + }, + { + "name": "parties", + "ID": 3, "constraints": [ { - "attribute": "city", - "value": "San Francisco", - "dataType": "string", - "matchType": "exact" + "attribute": "seats", + "value": "10", + "dataType": "int", + "matchType": "LT" } ] }, { - "type": "airports", + "name": "resolutions", + "ID": 4, "constraints": [ { - "attribute": "state", - "value": "HI", + "attribute": "date", + "value": "mei", "dataType": "string", - "matchType": "exact" + "matchType": "contains" } ] + }, + { + "name": "resolutions", + "ID": 5, + "constraints": [] + }, + { + "name": "parties", + "ID": 6, + "constraints": [] } + , + { + "name": "parliament", + "ID": 7, + "constraints": [] + } + ], + "groupBys": [], "relations": [ { - "type": "flights", + "ID": 0, + "name": "part_of", "depth": { "min": 1, - "max": 3 + "max": 1 }, - "entityFrom": 2, - "entityTo": 1, - "constraints": [ - { - "attribute": "Day", - "value": "15", - "dataType": "int", - "matchType": "EQ" - } - ] + "fromType": "entity", + "fromId": 0, + "toType": "entity", + "toID": 1, + "constraints": [] }, { - "type": "flights", + "ID": 1, + "name": "part_of", "depth": { "min": 1, "max": 1 }, - "entityFrom": 0, - "entityTo": -1, + "fromType": "entity", + "fromId": 2, + "toType": "entity", + "toID": 1, + "constraints": [] + }, + { + "ID": 2, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 2, + "toType": "entity", + "toID": 3, + "constraints": [] + }, + { + "ID": 3, + "name": "submits", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 2, + "toType": "entity", + "toID": 4, + "constraints": [] + }, + { + "ID": 4, + "name": "submits", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 0, + "toType": "entity", + "toID": 5, + "constraints": [] + }, + { + "ID": 5, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 0, + "toType": "entity", + "toID": 6, + "constraints": [] + } + , + { + "ID": 6, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 7, + "toType": "entity", + "toID": 6, "constraints": [] } ], @@ -763,57 +1324,138 @@ func TestTwoRelations(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `LET n2 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n2 FOR v, e, p IN 1..3 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER v.city == "San Francisco" FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET n0 = (FOR x IN airports FILTER x.city == "New York" RETURN x)LET r1 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), flatten(r1[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), flatten(r1[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) + correctConvertedResult := []byte(`LET result = ( + FOR e_3 IN parties + FILTER e_3.seats < 10 + LET e2 = ( + FOR e_2 IN parliament + FOR r2 IN member_of + FILTER r2._from == e_2._id AND r2._to == e_3._id + LET e1 = ( + FOR e_1 IN commissions + FOR r1 IN part_of + FILTER r1._from == e_2._id AND r1._to == e_1._id + LET e0 = ( + FOR e_0 IN parliament + FOR r0 IN part_of + FILTER e_0.name LIKE "%Geert%" + FILTER r0._from == e_0._id AND r0._to == e_1._id + LET e5 = ( + FOR e_5 IN resolutions + FOR r4 IN submits + FILTER r4._from == e_0._id AND r4._to == e_5._id + FILTER length(e_5) != 0 AND length(r4) != 0 + RETURN {"nodes":union_distinct([e_5], []), "rel": union_distinct([r4], []), "mod": union_distinct([], [])} + ) + LET e6 = ( + FOR e_6 IN parties + FOR r5 IN member_of + FILTER r5._from == e_0._id AND r5._to == e_6._id + LET e7 = ( + FOR e_7 IN parliament + FOR r6 IN member_of + FILTER r6._from == e_7._id AND r6._to == e_6._id + FILTER length(e_7) != 0 AND length(r6) != 0 + RETURN {"nodes":union_distinct([e_7], []), "rel": union_distinct([r6], []), "mod": union_distinct([], [])} + ) + FILTER length(e7) != 0 AND length(e_6) != 0 AND length(r5) != 0 + RETURN {"nodes":union_distinct(flatten(e7[**].nodes), [e_6]), "rel": union_distinct(flatten(e7[**].rel), [r5]), "mod": union_distinct(flatten(e7[**].mod), [])} + ) + FILTER length(e5) != 0 AND length(e6) != 0 AND length(e_0) != 0 AND length(r0) != 0 + RETURN {"nodes":union_distinct(flatten(e5[**].nodes), flatten(e6[**].nodes), [e_0]), "rel": union_distinct(flatten(e5[**].rel), flatten(e6[**].rel), [r0]), "mod": union_distinct(flatten(e5[**].mod), flatten(e6[**].mod), [])} + ) + FILTER length(e0) != 0 AND length(e_1) != 0 AND length(r1) != 0 + RETURN {"nodes":union_distinct(flatten(e0[**].nodes), [e_1]), "rel": union_distinct(flatten(e0[**].rel), [r1]), "mod": union_distinct(flatten(e0[**].mod), [])} + ) + LET e4 = ( + FOR e_4 IN resolutions + FOR r3 IN submits + FILTER e_4.date LIKE "%mei%" + FILTER r3._from == e_2._id AND r3._to == e_4._id + FILTER length(e_4) != 0 AND length(r3) != 0 + RETURN {"nodes":union_distinct([e_4], []), "rel": union_distinct([r3], []), "mod": union_distinct([], [])} + ) + FILTER length(e1) != 0 AND length(e4) != 0 AND length(e_2) != 0 AND length(r2) != 0 + RETURN {"nodes":union_distinct(flatten(e1[**].nodes), flatten(e4[**].nodes), [e_2]), "rel": union_distinct(flatten(e1[**].rel), flatten(e4[**].rel), [r2]), "mod": union_distinct(flatten(e1[**].mod), flatten(e4[**].mod), [])} + ) + FILTER length(e2) != 0 AND length(e_3) != 0 + RETURN {"nodes":union_distinct(flatten(e2[**].nodes), [e_3]), "rel": union_distinct(flatten(e2[**].rel), []), "mod": union_distinct(flatten(e2[**].mod), [])} +) +LET nodes = union_distinct(flatten(result[**].nodes),[]) +LET edges = union_distinct(flatten(result[**].rel),[]) +RETURN {"vertices":nodes,"edges":edges}`) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* -Tests a relation with only a to node +Tests two entities (one type) with one entity filter and one relation filter +Query description: Give me all airports, in the state "HI", connected to any other airport by flight, but only the flights on "day" 15 t: *testing.T, makes go recognise this as a test */ -func TestRelationWithOnlyToNode(t *testing.T) { +func TestTwoEntitiesOneEntityFilterOneRelationFilter(t *testing.T) { // Setup for test // Create query conversion service service := NewService() query := []byte(`{ - "return": { - "entities": [ - 0 - ], - "relations": [ - 0 - ] - }, + "return": { "entities": [ - { - "type": "airports", - "constraints": [ - { - "attribute": "city", - "value": "San Francisco", - "dataType": "string", - "matchType": "exact" - } - ] - } + 0, + 1 ], "relations": [ - { - "type": "flights", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": -1, - "entityTo": 0, - "constraints": [] - } - ], - "limit": 5000 - }`) + 0 + ] + }, + "entities": [ + { + "ID": 0, + "name": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "string", + "matchType": "exact" + } + ] + }, + { + "ID": 1, + "name": "airports", + "constraints":[] + } + ], + "relations": [ + { + "ID": 0, + "name": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "FromType": "entity", + "fromID": 0, + "ToType": "entity", + "toID": 1, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "int", + "matchType": "EQ", + "inType": "", + "inID": -1 + } + ] + } + ], + "groupBys": [], + "limit": 5000 + }`) // Unmarshall the incoming message into an IncomingJSONQuery object var JSONQuery entity.IncomingQueryJSON @@ -825,186 +1467,31 @@ func TestRelationWithOnlyToNode(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.city == "San Francisco" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 INBOUND x flights OPTIONS { uniqueEdges: "path" }LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) -} - -/* -Tests too manu return entities - t: *testing.T, makes go recognise this as a test -*/ -func TestTooManyReturnEntities(t *testing.T) { - // Setup for test - // Create query conversion service - service := NewService() - - query := []byte(`{ - "return": { - "entities": [ - 0, - 1, - 2 - ], - "relations": [ - 0 - ] - }, - "entities": [ - { - "type": "airports", - "constraints": [ - { - "attribute": "city", - "value": "San Francisco", - "dataType": "string", - "matchType": "exact" - } - ] - } - ], - "relations": [ - { - "type": "flights", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": -1, - "entityTo": 0, - "constraints": [] - } - ], - "limit": 5000 - }`) - - // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON - json.Unmarshal(query, &JSONQuery) - - _, err := service.ConvertQuery(&JSONQuery) - - // Assert that there is no error - assert.Equal(t, errors.New("non-existing entity referenced in return"), err) + correctConvertedResult := []byte(`LET result = ( + FOR e_0 IN airports + FILTER e_0.state == "HI" + LET e1 = ( + FOR e_1 IN airports + FOR r0 IN flights + FILTER r0.Day == 15 + FILTER r0._from == e_0._id AND r0._to == e_1._id + FILTER length(e_1) != 0 AND length(r0) != 0 + RETURN {"nodes":union_distinct([e_1], []), "rel": union_distinct([r0], []), "mod": union_distinct([], [])} + ) + FILTER length(e1) != 0 AND length(e_0) != 0 + RETURN {"nodes":union_distinct(flatten(e1[**].nodes), [e_0]), "rel": union_distinct(flatten(e1[**].rel), []), "mod": union_distinct(flatten(e1[**].mod), [])} +) +LET nodes = union_distinct(flatten(result[**].nodes),[]) +LET edges = union_distinct(flatten(result[**].rel),[]) +RETURN {"vertices":nodes,"edges":edges}`) + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } -/* -Tests too manu return relations - t: *testing.T, makes go recognise this as a test -*/ -func TestTooManyReturnRelations(t *testing.T) { - // Setup for test - // Create query conversion service - service := NewService() - - query := []byte(`{ - "return": { - "entities": [ - 0 - ], - "relations": [ - 0, - 1, - 2 - ] - }, - "entities": [ - { - "type": "airports", - "constraints": [ - { - "attribute": "city", - "value": "San Francisco", - "dataType": "string", - "matchType": "exact" - } - ] - } - ], - "relations": [ - { - "type": "flights", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": -1, - "entityTo": 0, - "constraints": [] - } - ], - "limit": 5000 - }`) - - // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON - json.Unmarshal(query, &JSONQuery) - - _, err := service.ConvertQuery(&JSONQuery) - - // Assert that there is no error - assert.Equal(t, errors.New("non-existing relation referenced in return"), err) -} - -/* -Tests negative return entities - t: *testing.T, makes go recognise this as a test -*/ -func TestNegativeReturnEntities(t *testing.T) { - // Setup for test - // Create query conversion service - service := NewService() - - query := []byte(`{ - "return": { - "entities": [ - 0, - -1 - ], - "relations": [ - 0, - 1, - 2 - ] - }, - "entities": [ - { - "type": "airports", - "constraints": [ - { - "attribute": "city", - "value": "San Francisco", - "dataType": "string", - "matchType": "exact" - } - ] - } - ], - "relations": [ - { - "type": "flights", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": -1, - "entityTo": 0, - "constraints": [] - } - ], - "limit": 5000 - }`) - - // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON - json.Unmarshal(query, &JSONQuery) - - _, err := service.ConvertQuery(&JSONQuery) - - // Assert that there is no error - assert.Equal(t, errors.New("non-existing entity referenced in return"), err) -} +//TODO +//FIX THESE TESTS, THEY'RE NOT THAT INTERESTING BUT SHOULD BE FIXED ANYWAY /* Tests a query with no relation field @@ -1016,110 +1503,29 @@ func TestNoRelationsField(t *testing.T) { service := NewService() query := []byte(`{ - "return": { - "entities": [ - 0 - ] - }, + "databaseName": "TweedeKamer", + "return": { "entities": [ - { - "type": "airports", - "constraints": [ - { - "attribute": "city", - "value": "San Francisco", - "dataType": "string", - "matchType": "exact" - } - ] - } - ], - "limit": 5000 - }`) - - // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON - json.Unmarshal(query, &JSONQuery) - - convertedResult, err := service.ConvertQuery(&JSONQuery) - - // Assert that there is no error - assert.NoError(t, err) - - // Assert that the result and the expected result are the same - correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.city == "San Francisco" RETURN x)LET nodes = first(RETURN UNION_DISTINCT(n0,[],[]))LET edges = first(RETURN UNION_DISTINCT([],[]))RETURN {"vertices":nodes, "edges":edges }` - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) -} - -/* -Tests a query with double WITH - t: *testing.T, makes go recognise this as a test -*/ -func TestDoubleWITH(t *testing.T) { - // Setup for test - // Create query conversion service - service := NewService() - - query := []byte(`{ - "databaseName": "test", - "return": { - "entities": [ - 0, - 1, - 2, - 3 - ], - "relations": [ - 0, - 1 - ] - }, - "entities": [ - { - "type": "kamerleden", - "constraints": [] - }, - { - "type": "partijen", - "constraints": [] - } - , - { - "type": "kamerleden", - "constraints": [] - }, - { - "type": "commissies", - "constraints": [] - } - ], - "relations": [ - { - "type": "lid_van", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": 0, - "entityTo": 1, - "constraints": [] - }, - { - "type": "onderdeel_van", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": 2, - "entityTo": 3, - "constraints": [] - } - ], - "limit": 5000, - "modifiers": [] - }`) + 0, + 1 + ] + }, + "entities": [ + { + "name": "parliament", + "ID": 0, + "constraints": [] + }, + { + "name": "parties", + "ID": 1, + "constraints": [] + } + ], + "groupBys": [], + "limit": 5000, + "modifiers": [] + }`) // Unmarshall the incoming message into an IncomingJSONQuery object var JSONQuery entity.IncomingQueryJSON @@ -1131,57 +1537,66 @@ func TestDoubleWITH(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := "WITH partijen, commissiesLET n0 = (FOR x IN kamerleden RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x lid_van OPTIONS { uniqueEdges: \"path\" }LIMIT 5000 RETURN DISTINCT p )LET n2 = (FOR x IN kamerleden RETURN x)LET r1 = (FOR x IN n2 FOR v, e, p IN 1..1 OUTBOUND x onderdeel_van OPTIONS { uniqueEdges: \"path\" }LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), flatten(r1[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), flatten(r1[**].edges), [],[]))RETURN {\"vertices\":nodes, \"edges\":edges }" - cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") - cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") - assert.Equal(t, correctConvertedResult, cleanedResult) + correctConvertedResult := `LET nodes = first(RETURN UNION_DISTINCT([],[])) + LET edges = first(RETURN UNION_DISTINCT([],[])) + RETURN {"vertices":nodes, "edges":edges }` + regExCleaner := regexp.MustCompile(`\s+`) + correctCleanedResult := regExCleaner.ReplaceAllString(string(correctConvertedResult), " ") + convertedCleanedResult := regExCleaner.ReplaceAllString(*convertedResult, " ") + assert.Equal(t, correctCleanedResult, convertedCleanedResult) } /* Tests an entity with a lower than -1 in a relation t: *testing.T, makes go recognise this as a test */ -func TestEntityFromLowerThanNegativeOneInRelation(t *testing.T) { +func TestIncorrectRelationFrom(t *testing.T) { // Setup for test // Create query conversion service service := NewService() query := []byte(`{ - "return": { - "entities": [ - 0 - ], - "relations": [ - 0 - ] - }, + "databaseName": "TweedeKamer", + "return": { "entities": [ - { - "type": "airports", - "constraints": [ - { - "attribute": "city", - "value": "San Francisco", - "dataType": "string", - "matchType": "exact" - } - ] - } + 0, + 1 ], "relations": [ - { - "type": "flights", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": -4, - "entityTo": 0, - "constraints": [] - } - ], - "limit": 5000 - }`) + 0 + ] + }, + "entities": [ + { + "name": "parliament", + "ID": 0, + "constraints": [] + }, + { + "name": "parties", + "ID": 1, + "constraints": [] + } + ], + "relations": [ + { + "ID": 0, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": -4, + "constraints":[] + } + ], + "groupBys": [], + "limit": 5000, + "modifiers": [] + }`) // Unmarshall the incoming message into an IncomingJSONQuery object var JSONQuery entity.IncomingQueryJSON @@ -1189,6 +1604,6 @@ func TestEntityFromLowerThanNegativeOneInRelation(t *testing.T) { _, err := service.ConvertQuery(&JSONQuery) - // Assert that there is no error - assert.NoError(t, err) + // Assert that there is an error + assert.Equal(t, errors.New("JSONQuery invalid"), err) } diff --git a/aql/fixIndices.go b/aql/fixIndices.go new file mode 100644 index 0000000000000000000000000000000000000000..1056cf4489acc6c97f0a091368228ccd7afb7f08 --- /dev/null +++ b/aql/fixIndices.go @@ -0,0 +1,21 @@ +package aql + +import ( + "git.science.uu.nl/graphpolaris/query-conversion/entity" +) + +func fixIndices(JSONQuery *entity.IncomingQueryJSON) (map[int]int, map[int]int, map[int]int) { + entityMap := make(map[int]int) + for i, e := range JSONQuery.Entities { + entityMap[e.ID] = i + } + relationMap := make(map[int]int) + for i, r := range JSONQuery.Relations { + relationMap[r.ID] = i + } + groupByMap := make(map[int]int) + for i, g := range JSONQuery.GroupBys { + groupByMap[g.ID] = i + } + return entityMap, relationMap, groupByMap +} diff --git a/aql/hierarchy.go b/aql/hierarchy.go new file mode 100644 index 0000000000000000000000000000000000000000..83931158035e4f5856f0dc6a1c2cfa2595e5f9e9 --- /dev/null +++ b/aql/hierarchy.go @@ -0,0 +1,112 @@ +package aql + +import ( + "git.science.uu.nl/graphpolaris/query-conversion/entity" +) + +// We use consts to define string to prevent typos +const ENTITYSTRING = "entity" +const RELATIONSTRING = "relation" +const GROUPBYSTRING = "groupBy" + +var relationdone map[int]bool + +func createHierarchy(JSONQuery *entity.IncomingQueryJSON, entityMap map[int]int, relationMap map[int]int) ([]entity.Tree, entity.QueryEntityStruct) { + var treeList []entity.Tree + relationdone = make(map[int]bool) + topNode := getTopNode(JSONQuery) + topTreeSelfTriple := getTripleFromNode(JSONQuery, topNode, entityMap, relationMap) + topTree := entity.Tree{ + Self: topTreeSelfTriple, + Parent: -1, + Children: []int{}, + } + treeList = append(treeList, topTree) + treeListIndex := len(treeList) - 1 + treeList = getChildrenFromTree(JSONQuery, treeList, treeListIndex, 0, entityMap) + return treeList, topNode +} + +/* +Get a entity that has only 1 relation attached an return its index +*/ +func getTopNode(JSONQuery *entity.IncomingQueryJSON) entity.QueryEntityStruct { + indexOfNodeToReturn := -1 + for i, node := range JSONQuery.Entities { + connectionCount := 0 + for _, relation := range JSONQuery.Relations { + if (relation.FromType == ENTITYSTRING && relation.FromID == node.ID) || (relation.ToType == ENTITYSTRING && relation.ToID == node.ID) { + connectionCount++ + } + } + if connectionCount == 1 { + indexOfNodeToReturn = i + return JSONQuery.Entities[indexOfNodeToReturn] + } + } + return JSONQuery.Entities[indexOfNodeToReturn] +} + +func getTripleFromNode(JSONQuery *entity.IncomingQueryJSON, node entity.QueryEntityStruct, entityMap map[int]int, relationMap map[int]int) entity.Triple { + var tripleToReturn entity.Triple + for _, relation := range JSONQuery.Relations { + // The initial node was our From so we set the Triple accordingly + // TODO + // If the To is not an entity we might have to do something different + if (relation.FromType == ENTITYSTRING && relation.FromID == node.ID) && relation.ToType == ENTITYSTRING { + tripleToReturn.FromNode = node + tripleToReturn.Rel = relation + tripleToReturn.ToNode = JSONQuery.Entities[entityMap[relation.ToID]] + } else if (relation.ToType == ENTITYSTRING && relation.ToID == node.ID) && relation.FromType == ENTITYSTRING { + tripleToReturn.FromNode = JSONQuery.Entities[entityMap[relation.FromID]] + tripleToReturn.Rel = relation + tripleToReturn.ToNode = node + } + } + relationdone[relationMap[tripleToReturn.Rel.ID]] = true + return tripleToReturn +} + +func getTripleFromRelation(JSONQuery *entity.IncomingQueryJSON, relation entity.QueryRelationStruct, entityMap map[int]int) entity.Triple { + var tripleToReturn entity.Triple + tripleToReturn.FromNode = JSONQuery.Entities[entityMap[relation.FromID]] + tripleToReturn.Rel = relation + tripleToReturn.ToNode = JSONQuery.Entities[entityMap[relation.ToID]] + return tripleToReturn +} + +func getChildrenFromTree(JSONQuery *entity.IncomingQueryJSON, treeList []entity.Tree, treeListIndex int, parentIndex int, entityMap map[int]int) []entity.Tree { + var childRelationTriples []entity.Triple + for i, relation := range JSONQuery.Relations { + // We found a relation that is not our parent relation so we can check if it matches on of our nodes + // If it matches one of the nodes we can add it + if _, ok := relationdone[i]; !ok { + if relation.FromType == ENTITYSTRING && relation.FromID == treeList[parentIndex].Self.FromNode.ID { + triple := getTripleFromRelation(JSONQuery, relation, entityMap) + childRelationTriples = append(childRelationTriples, triple) + relationdone[i] = true + } else if relation.ToType == ENTITYSTRING && relation.ToID == treeList[parentIndex].Self.ToNode.ID { + triple := getTripleFromRelation(JSONQuery, relation, entityMap) + childRelationTriples = append(childRelationTriples, triple) + relationdone[i] = true + } + } + } + // We now have all our children, so we can now make those trees and find their children + // We can now also add the indices to the list of children from the tree calling this function + if len(childRelationTriples) != 0 { + for _, triple := range childRelationTriples { + childTree := entity.Tree{ + Self: triple, + Parent: parentIndex, + Children: []int{}, + } + treeList = append(treeList, childTree) + // We get the new treeListIndex, which we can now add to the list of children from the tree calling this function + treeListIndex = len(treeList) - 1 + treeList[parentIndex].Children = append(treeList[parentIndex].Children, treeListIndex) + treeList = getChildrenFromTree(JSONQuery, treeList, treeListIndex, treeListIndex, entityMap) + } + } + return treeList +} diff --git a/aql/hierarchy_test.go b/aql/hierarchy_test.go new file mode 100644 index 0000000000000000000000000000000000000000..073825408a750f9fdd6b6b3593851380d1032a58 --- /dev/null +++ b/aql/hierarchy_test.go @@ -0,0 +1,324 @@ +package aql + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + + "git.science.uu.nl/graphpolaris/query-conversion/entity" + "github.com/stretchr/testify/assert" +) + +func TestHierarchyBasic(t *testing.T) { + // Setup for test + // Create query conversion service + query := []byte(`{ + "return": { + "entities": [ + 0, + 1, + 2, + 3, + 4 + ], + "relations": [ + 0, + 1, + 2, + 3 + ] + }, + "entities": [ + { + "name": "parliament", + "ID": 0, + "constraints": [ + { + "attribute": "name", + "value": "Geert", + "dataType": "string", + "matchType": "CONTAINS" + } + ] + }, + { + "name": "commissions", + "ID": 1, + "constraints": [] + }, + { + "name": "parliament", + "ID": 2, + "constraints": [] + }, + { + "name": "parties", + "ID": 3, + "constraints": [ + { + "attribute": "seats", + "value": "10", + "dataType": "int", + "matchType": "LT" + } + ] + }, + { + "name": "resolutions", + "ID": 4, + "constraints": [ + { + "attribute": "date", + "value": "mei", + "dataType": "string", + "matchType": "CONTAINS" + } + ] + } + ], + "groupBys": [], + "relations": [ + { + "ID": 0, + "name": "part_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 0, + "toType": "entity", + "toID": 1, + "constraints": [] + }, + { + "ID": 1, + "name": "part_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 2, + "toType": "entity", + "toID": 1, + "constraints": [] + }, + { + "ID": 2, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 2, + "toType": "entity", + "toID": 3, + "constraints": [] + }, + { + "ID": 3, + "name": "submits", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 2, + "toType": "entity", + "toID": 4, + "constraints": [] + } + ], + "limit": 5000 + } + `) + + // Unmarshall the incoming message into an IncomingJSONQuery object + var JSONQuery entity.IncomingQueryJSON + json.Unmarshal(query, &JSONQuery) + // Get the hierarchy and turn it into JSON so we can turn it into strings later + entityMap, relationMap, _ := fixIndices(&JSONQuery) + treeList, topNode := createHierarchy(&JSONQuery, entityMap, relationMap) + jsonTopNode, err := json.Marshal(topNode) + if err != nil { + fmt.Println("Marshalling went wrong") + } + jsonTreeList, err := json.Marshal(treeList) + if err != nil { + fmt.Println("Marshalling went wrong") + } + // These are the expected (correct) outputs + correctTopNode := []byte(`{"ID":0,"Name":"parliament","Constraints":[{"Attribute":"name","Value":"Geert","DataType":"string","MatchType":"CONTAINS","InID":0,"InType":""}]}`) + correctTreeList := []byte(`[ + { + "Self": { + "FromNode": { + "ID": 0, + "Name": "parliament", + "Constraints": [ + { + "Attribute": "name", + "Value": "Geert", + "DataType": "string", + "MatchType": "CONTAINS", + "InID": 0, + "InType": "" + } + ] + }, + "Rel": { + "ID": 0, + "Name": "part_of", + "FromType": "entity", + "FromID": 0, + "ToType": "entity", + "ToID": 1, + "Depth": { + "Min": 1, + "Max": 1 + }, + "Constraints": [] + }, + "ToNode": { + "ID": 1, + "Name": "commissions", + "Constraints": [] + } + }, + "Parent": -1, + "Children": [ + 1 + ] + }, + { + "Self": { + "FromNode": { + "ID": 2, + "Name": "parliament", + "Constraints": [] + }, + "Rel": { + "ID": 1, + "Name": "part_of", + "FromType": "entity", + "FromID": 2, + "ToType": "entity", + "ToID": 1, + "Depth": { + "Min": 1, + "Max": 1 + }, + "Constraints": [] + }, + "ToNode": { + "ID": 1, + "Name": "commissions", + "Constraints": [] + } + }, + "Parent": 0, + "Children": [ + 2, + 3 + ] + }, + { + "Self": { + "FromNode": { + "ID": 2, + "Name": "parliament", + "Constraints": [] + }, + "Rel": { + "ID": 2, + "Name": "member_of", + "FromType": "entity", + "FromID": 2, + "ToType": "entity", + "ToID": 3, + "Depth": { + "Min": 1, + "Max": 1 + }, + "Constraints": [] + }, + "ToNode": { + "ID": 3, + "Name": "parties", + "Constraints": [ + { + "Attribute": "seats", + "Value": "10", + "DataType": "int", + "MatchType": "LT", + "InID": 0, + "InType": "" + } + ] + } + }, + "Parent": 1, + "Children": [] + }, + { + "Self": { + "FromNode": { + "ID": 2, + "Name": "parliament", + "Constraints": [] + }, + "Rel": { + "ID": 3, + "Name": "submits", + "FromType": "entity", + "FromID": 2, + "ToType": "entity", + "ToID": 4, + "Depth": { + "Min": 1, + "Max": 1 + }, + "Constraints": [] + }, + "ToNode": { + "ID": 4, + "Name": "resolutions", + "Constraints": [ + { + "Attribute": "date", + "Value": "mei", + "DataType": "string", + "MatchType": "CONTAINS", + "InID": 0, + "InType": "" + } + ] + } + }, + "Parent": 1, + "Children": [] + } + ]`) + // Clean up the input and expected results + cleanedTopNode := strings.ReplaceAll(string(jsonTopNode), "\n", "") + cleanedTopNode = strings.ReplaceAll(cleanedTopNode, "\t", "") + cleanedTopNode = strings.ReplaceAll(cleanedTopNode, " ", "") + + cleanedTreeList := strings.ReplaceAll(string(jsonTreeList), "\n", "") + cleanedTreeList = strings.ReplaceAll(cleanedTreeList, "\t", "") + cleanedTreeList = strings.ReplaceAll(cleanedTreeList, " ", "") + + cleanedCorrectTopNode := strings.ReplaceAll(string(correctTopNode), "\n", "") + cleanedCorrectTopNode = strings.ReplaceAll(cleanedCorrectTopNode, "\t", "") + cleanedCorrectTopNode = strings.ReplaceAll(cleanedCorrectTopNode, " ", "") + + cleanedCorrectTreeList := strings.ReplaceAll(string(correctTreeList), "\n", "") + cleanedCorrectTreeList = strings.ReplaceAll(cleanedCorrectTreeList, "\t", "") + cleanedCorrectTreeList = strings.ReplaceAll(cleanedCorrectTreeList, " ", "") + + assert.Equal(t, cleanedCorrectTopNode, cleanedTopNode) + assert.Equal(t, cleanedCorrectTreeList, cleanedTreeList) +} diff --git a/aql/mockConvertQuery.go b/aql/mockConvertQuery.go index 8a23e4c8a1896fdab0475a361da09a3f6716e4b6..91139b5c0ecc1beef29e76d2648298b6092e2df4 100644 --- a/aql/mockConvertQuery.go +++ b/aql/mockConvertQuery.go @@ -39,7 +39,7 @@ func (s *MockService) ConvertQuery(JSONQuery *entity.IncomingQueryJSON) (*string if !s.throwError { return &mockQuery, nil } - return nil, errors.New("Failed to convert query") + return nil, errors.New("failed to convert query") } /* diff --git a/cypher/convertQuery.go b/cypher/convertQuery.go index 6c108a9a5fc2573b73e22fa62689b38281510653..0284e58b031d2b64d1f4c1b9bde218b2137a60a7 100644 --- a/cypher/convertQuery.go +++ b/cypher/convertQuery.go @@ -10,7 +10,7 @@ import ( "fmt" "strings" - "git.science.uu.nl/graphpolaris/query-conversion/entity" + "git.science.uu.nl/graphpolaris/query-conversion/entity/entitycypher" ) /* @@ -18,7 +18,7 @@ ConvertQuery converts an IncomingQueryJSON object into AQL JSONQuery: *entity.IncomingQueryJSON, the query to be converted to AQL Returns: (*string, error), the AQL query and a possible error */ -func (s *Service) ConvertQuery(JSONQuery *entity.IncomingQueryJSON) (*string, error) { +func (s *Service) ConvertQuery(JSONQuery *entitycypher.IncomingQueryJSON) (*string, error) { // Check to make sure all indexes exist // How many entities are there @@ -80,7 +80,7 @@ createQuery generates a query based on the json file provided JSONQuery: *entity.IncomingQueryJSON, jsonQuery 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 { +func createQuery(JSONQuery *entitycypher.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 @@ -200,7 +200,7 @@ createNodeLet generates a 'LET' statement for a node related query 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 createNodeMatch(node *entity.QueryEntityStruct, name *string) *string { +func createNodeMatch(node *entitycypher.QueryEntityStruct, name *string) *string { // hier zat een newline header := fmt.Sprintf("MATCH (%v:%v) ", *name, node.Type) constraints := *createConstraintStatements(&node.Constraints, *name) @@ -218,7 +218,7 @@ createRelationLetWithFromEntity generates a 'LET' statement for relations with a outbound: bool, checks if the relation is inbound or outbound Return: *string, a string containing a single LET-statement in AQL */ -func createRelationMatch(relation *entity.QueryRelationStruct, relationName string, pathName string, entities *[]entity.QueryEntityStruct, limit int, outbound bool) *string { +func createRelationMatch(relation *entitycypher.QueryRelationStruct, relationName string, pathName string, entities *[]entitycypher.QueryEntityStruct, limit int, outbound bool) *string { relationReturn := "" var relationBounds int if outbound { diff --git a/cypher/convertQueryBenchmark_test.go b/cypher/convertQueryBenchmark_test.go index d1a539722629a52899d790051b19932f2ab836d7..5e14480faaab79b77b0813e02ac740dfd266b579 100644 --- a/cypher/convertQueryBenchmark_test.go +++ b/cypher/convertQueryBenchmark_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "git.science.uu.nl/graphpolaris/query-conversion/entity" + "git.science.uu.nl/graphpolaris/query-conversion/entity/entitycypher" ) func BenchmarkConvertEmptyQuery(b *testing.B) { @@ -23,7 +23,7 @@ func BenchmarkConvertEmptyQuery(b *testing.B) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) b.ResetTimer() @@ -63,7 +63,7 @@ func BenchmarkConvertOneAttributeQuery(b *testing.B) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) b.ResetTimer() @@ -158,7 +158,7 @@ func BenchmarkConvertTwoRelationQuery(b *testing.B) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) b.ResetTimer() diff --git a/cypher/convertQuery_test.go b/cypher/convertQuery_test.go index 028455e58b73f596d7f8cda6254bfe9c9ba5e02b..fffb0a0da17d9550ea0a71ae6cdd993dc57a10a8 100644 --- a/cypher/convertQuery_test.go +++ b/cypher/convertQuery_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "git.science.uu.nl/graphpolaris/query-conversion/entity" + "git.science.uu.nl/graphpolaris/query-conversion/entity/entitycypher" "github.com/stretchr/testify/assert" ) @@ -26,7 +26,7 @@ func TestEmptyQueryConversion(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) convertedResult, err := service.ConvertQuery(&JSONQuery) @@ -69,7 +69,7 @@ func TestEntityOneAttributeQuery(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) convertedResult, err := service.ConvertQuery(&JSONQuery) @@ -78,7 +78,7 @@ func TestEntityOneAttributeQuery(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `MATCH (n0:airports)WHERE n0.state = "HI" RETURN n0` + correctConvertedResult := `MATCH (n0:airports) WHERE n0.state = "HI" RETURN n0` cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") assert.Equal(t, correctConvertedResult, cleanedResult) @@ -134,7 +134,7 @@ func TestRelationWithConstraint(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) convertedResult, err := service.ConvertQuery(&JSONQuery) @@ -143,7 +143,7 @@ func TestRelationWithConstraint(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `MATCH (n0:airports)WHERE n0.state = "HI" MATCH p0 = (n0)-[r0:flights*1..1]->()WHERE r0.Day = 15 RETURN n0,p0;` + correctConvertedResult := `MATCH (n0:airports) WHERE n0.state = "HI" MATCH p0 = (n0)-[r0:flights*1..1]->() WHERE r0.Day = 15 RETURN n0,p0;` cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") assert.Equal(t, correctConvertedResult, cleanedResult) @@ -462,7 +462,7 @@ func TestRelationWithInOutConstraint(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) convertedResult, err := service.ConvertQuery(&JSONQuery) @@ -471,7 +471,7 @@ func TestRelationWithInOutConstraint(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `MATCH (n1:airports)WHERE n1.state = "HI" MATCH p0 = (n1)-[r0:flights*1..3]->(n0)WHERE r0.Day = 15 RETURN n1,n0,p0;` + correctConvertedResult := `MATCH (n1:airports) WHERE n1.state = "HI" MATCH p0 = (n1)-[r0:flights*1..3]->(n0) WHERE r0.Day = 15 RETURN n1,n0,p0;` cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") assert.Equal(t, correctConvertedResult, cleanedResult) @@ -562,7 +562,7 @@ func TestTwoRelations(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) convertedResult, err := service.ConvertQuery(&JSONQuery) @@ -571,7 +571,7 @@ func TestTwoRelations(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `MATCH (n2:airports)WHERE n2.state = "HI" MATCH p0 = (n2)-[r0:flights*1..3]->(n1)WHERE r0.Day = 15 RETURN n2,n1,p0;MATCH (n0:airports)WHERE n0.city = "New York" MATCH p0 = (n0)-[r0:flights*1..1]->()RETURN n0,p0;` + correctConvertedResult := `MATCH (n2:airports) WHERE n2.state = "HI" MATCH p0 = (n2)-[r0:flights*1..3]->(n1) WHERE r0.Day = 15 RETURN n2,n1,p0; MATCH (n0:airports) WHERE n0.city = "New York" MATCH p0 = (n0)-[r0:flights*1..1]->() RETURN n0,p0;` cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") assert.Equal(t, correctConvertedResult, cleanedResult) @@ -620,7 +620,7 @@ func TestRelationWithOnlyToNode(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) convertedResult, err := service.ConvertQuery(&JSONQuery) @@ -629,7 +629,7 @@ func TestRelationWithOnlyToNode(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `MATCH (n0:airports)WHERE n0.city = "San Francisco" MATCH p0 = (n0)-[r0:flights*1..1]->()RETURN n0,p0;` + correctConvertedResult := `MATCH (n0:airports) WHERE n0.city = "San Francisco" MATCH p0 = (n0)-[r0:flights*1..1]->() RETURN n0,p0;` cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") assert.Equal(t, correctConvertedResult, cleanedResult) @@ -680,7 +680,7 @@ func TestTooManyReturnEntities(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) _, err := service.ConvertQuery(&JSONQuery) @@ -734,7 +734,7 @@ func TestTooManyReturnRelations(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) _, err := service.ConvertQuery(&JSONQuery) @@ -789,7 +789,7 @@ func TestNegativeReturnEntities(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) _, err := service.ConvertQuery(&JSONQuery) @@ -826,7 +826,7 @@ func TestNoRelationsField(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) convertedResult, err := service.ConvertQuery(&JSONQuery) @@ -835,7 +835,7 @@ func TestNoRelationsField(t *testing.T) { assert.NoError(t, err) // Assert that the result and the expected result are the same - correctConvertedResult := `MATCH (n0:airports)WHERE n0.city = "San Francisco" RETURN n0` + correctConvertedResult := `MATCH (n0:airports) WHERE n0.city = "San Francisco" RETURN n0` cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") assert.Equal(t, correctConvertedResult, cleanedResult) @@ -884,7 +884,7 @@ func TestEntityFromLowerThanNegativeOneInRelation(t *testing.T) { }`) // Unmarshall the incoming message into an IncomingJSONQuery object - var JSONQuery entity.IncomingQueryJSON + var JSONQuery entitycypher.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) _, err := service.ConvertQuery(&JSONQuery) diff --git a/cypher/createConstraints.go b/cypher/createConstraints.go index 6ec58a7be4cc70fd9210cb9f8e715dea189df463..d88a374f7705c5c322327784a0b41a6c7e7252b3 100644 --- a/cypher/createConstraints.go +++ b/cypher/createConstraints.go @@ -3,7 +3,7 @@ package cypher import ( "fmt" - "git.science.uu.nl/graphpolaris/query-conversion/entity" + "git.science.uu.nl/graphpolaris/query-conversion/entity/entitycypher" ) /* createConstraintStatements generates the appropriate amount of constraint lines calling createConstraingBoolExpression @@ -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 */ -func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, name string) *string { +func createConstraintStatements(constraints *[]entitycypher.QueryConstraintStruct, name string) *string { s := "" if len(*constraints) == 0 { return &s @@ -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 */ -func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, name string) *string { +func createConstraintBoolExpression(constraint *entitycypher.QueryConstraintStruct, name string) *string { var ( match string value string diff --git a/entity/entitycypher/queryStructCypher.go b/entity/entitycypher/queryStructCypher.go new file mode 100644 index 0000000000000000000000000000000000000000..6db12d36a5974731d7bf9a0c4bfe4f457353f5ed --- /dev/null +++ b/entity/entitycypher/queryStructCypher.go @@ -0,0 +1,62 @@ +package entitycypher + +// IncomingQueryJSON describes the query coming into the service in JSON format +type IncomingQueryJSON struct { + DatabaseName string + Return QueryReturnStruct + Entities []QueryEntityStruct + Relations []QueryRelationStruct + // Limit is for limiting the amount of paths AQL will return in a relation let statement + Limit int + Modifiers []QueryModifierStruct +} + +// QueryReturnStruct holds the indices of the entities and relations that need to be returned +type QueryReturnStruct struct { + Entities []int + Relations []int + //Modifiers []int +} + +// QueryEntityStruct encapsulates a single entity with its corresponding constraints +type QueryEntityStruct struct { + Type string + Constraints []QueryConstraintStruct +} + +// QueryRelationStruct encapsulates a single relation with its corresponding constraints +type QueryRelationStruct struct { + Type string + EntityFrom int + EntityTo int + Depth QuerySearchDepthStruct + Constraints []QueryConstraintStruct +} + +// QueryModifierStruct encapsulates a single modifier with its corresponding constraints +type QueryModifierStruct struct { + Type string // SUM COUNT AVG + SelectedType string // node relation + SelectedTypeID int // ID of the enitity or relation + AttributeIndex int // = -1 if its the node or relation, = > -1 if an attribute is selected + InType string + InID int +} + +// QuerySearchDepthStruct holds the range of traversals for the relation +type QuerySearchDepthStruct struct { + Min int + Max int +} + +// QueryConstraintStruct holds the information of the constraint +// Constraint datatypes +// string MatchTypes: exact/contains/startswith/endswith +// int MatchTypes: GT/LT/EQ +// bool MatchTypes: EQ/NEQ +type QueryConstraintStruct struct { + Attribute string + Value string + DataType string + MatchType string +} diff --git a/entity/hierarchyStruct.go b/entity/hierarchyStruct.go new file mode 100644 index 0000000000000000000000000000000000000000..fe86cdbdb07f2426b1e76cd117cbd430fbbc1b76 --- /dev/null +++ b/entity/hierarchyStruct.go @@ -0,0 +1,38 @@ +package entity + +type PdictList []Pdict + +type Pdict struct { + Typename string + Pointer int +} + +type Triple struct { + FromNode QueryEntityStruct + Rel QueryRelationStruct + ToNode QueryEntityStruct +} + +type Tree struct { + Self Triple + Parent int + Children []int +} + +func (p PdictList) Len() int { + return len(p) +} + +func (p PdictList) Less(i, j int) bool { + if p[i].Typename < p[j].Typename { + return true + } else if p[i].Typename == p[j].Typename && p[i].Pointer < p[j].Pointer { + return true + } else { + return false + } +} + +func (p PdictList) Swap(i, j int) { + p[i], p[j] = p[j], p[i] +} diff --git a/entity/queryStruct.go b/entity/queryStruct.go index 7b6c2760ac2ad7c0b1a002c516559011a85cdd5d..b74e4f5ae8cba801aea1fa33d9b0a510400dfaee 100644 --- a/entity/queryStruct.go +++ b/entity/queryStruct.go @@ -2,10 +2,12 @@ package entity // IncomingQueryJSON describes the query coming into the service in JSON format type IncomingQueryJSON struct { - DatabaseName string - Return QueryReturnStruct - Entities []QueryEntityStruct - Relations []QueryRelationStruct + DatabaseName string + Return QueryReturnStruct + Entities []QueryEntityStruct + Relations []QueryRelationStruct + GroupBys []QueryGroupByStruct + MachineLearning []QueryMLStruct // Limit is for limiting the amount of paths AQL will return in a relation let statement Limit int Modifiers []QueryModifierStruct @@ -15,36 +17,40 @@ type IncomingQueryJSON struct { type QueryReturnStruct struct { Entities []int Relations []int + GroupBys []int //Modifiers []int } // QueryEntityStruct encapsulates a single entity with its corresponding constraints type QueryEntityStruct struct { - Type string + ID int + Name string Constraints []QueryConstraintStruct } // QueryRelationStruct encapsulates a single relation with its corresponding constraints type QueryRelationStruct struct { - Type string - EntityFrom int - EntityTo int + ID int + Name string + FromType string + FromID int + ToType string + ToID int Depth QuerySearchDepthStruct Constraints []QueryConstraintStruct } -// QueryModifierStruct encapsulates a single modifier with its corresponding constraints -type QueryModifierStruct struct { - Type string // SUM COUNT AVG - SelectedType string // node relation - SelectedTypeID int // ID of the enitity or relation - AttributeIndex int // = -1 if its the node or relation, = > -1 if an attribute is selected -} - -// QuerySearchDepthStruct holds the range of traversals for the relation -type QuerySearchDepthStruct struct { - Min int - Max int +type QueryGroupByStruct struct { + ID int + GroupType string + GroupID int + GroupAttribute string + ByType string + ByID int + ByAttribute string + AppliedModifier string + RelationID int + Constraints []QueryConstraintStruct } // QueryConstraintStruct holds the information of the constraint @@ -57,4 +63,25 @@ type QueryConstraintStruct struct { Value string DataType string MatchType string + InID int + InType string +} + +type QueryMLStruct struct { + Queuename string + Parameters []string +} + +// QueryModifierStruct encapsulates a single modifier with its corresponding constraints +type QueryModifierStruct struct { + Type string // SUM COUNT AVG + SelectedType string // entity relation + SelectedTypeID int // ID of the enitity or relation + AttributeIndex int // = -1 if its the node or relation, = > -1 if an attribute is selected +} + +// QuerySearchDepthStruct holds the range of traversals for the relation +type QuerySearchDepthStruct struct { + Min int + Max int } diff --git a/entity/queryStructValidator.go b/entity/queryStructValidator.go new file mode 100644 index 0000000000000000000000000000000000000000..2d44ce86effe0459952591ccd86a4f6f17107bbc --- /dev/null +++ b/entity/queryStructValidator.go @@ -0,0 +1,147 @@ +package entity + +import ( + "errors" +) + +const ENTITYSTRING = "entity" +const RELATIONSTRING = "relation" +const GROUPBYSTRING = "groupBy" + +/* +sliceContains checks if a slice contains the input + JSONQuery: IncomingQueryJSON, the query in JSON to be validated + Return: []error, a list of specific transgression in this specific JSON query' +*/ +func ValidateStruct(JSONQuery IncomingQueryJSON) []error { + ret := make([]error, 0) + minEntityID, maxEntityID := getMinAndMaxEntityID(JSONQuery.Entities) + minRelationID, maxRelationID := getMinAndMaxRelationID(JSONQuery.Relations) + minGroupByID, maxGroupByID := getMinAndMaxGroupByID(JSONQuery.GroupBys) + ret = append(ret, getIllegalToFromInRelation(JSONQuery, minEntityID, maxEntityID, minGroupByID, maxGroupByID)...) + if len(JSONQuery.GroupBys) != 0 { + ret = append(ret, getIllegalToFromInGroupBy(JSONQuery, minEntityID, maxEntityID, minRelationID, maxRelationID)...) + } + return ret +} + +func getIllegalToFromInRelation(JSONQuery IncomingQueryJSON, minEntityID int, maxEntityID int, minGroupByID int, maxGroupByID int) []error { + ret := make([]error, 0) + for _, rel := range JSONQuery.Relations { + toEntityValid := relationToValid(rel, ENTITYSTRING, minEntityID, maxEntityID) + toGroupByValid := relationToValid(rel, GROUPBYSTRING, minGroupByID, maxGroupByID) + fromEntityValid := relationFromValid(rel, ENTITYSTRING, minEntityID, maxEntityID) + fromGroupByValid := relationFromValid(rel, GROUPBYSTRING, minGroupByID, maxGroupByID) + if !(toEntityValid || toGroupByValid) { + err := errors.New("relation has invalid TO type and/or ID") + ret = append(ret, err) + } + if !(fromEntityValid || fromGroupByValid) { + err := errors.New("relation has invalid FROM type and/or ID") + ret = append(ret, err) + } + } + return ret +} + +func getIllegalToFromInGroupBy(JSONQuery IncomingQueryJSON, minEntityID int, maxEntityID int, minRelationID int, maxRelationID int) []error { + ret := make([]error, 0) + for _, groupBy := range JSONQuery.GroupBys { + groupEntityValid := groupByGroupValid(groupBy, ENTITYSTRING, minEntityID, maxEntityID) + groupRelationValid := groupByGroupValid(groupBy, RELATIONSTRING, minRelationID, maxRelationID) + byEntityValid := groupByByValid(groupBy, ENTITYSTRING, minEntityID, maxEntityID) + byRelationValid := groupByByValid(groupBy, RELATIONSTRING, minRelationID, maxRelationID) + if !(groupEntityValid || groupRelationValid) { + err := errors.New("relation has invalid TO type and/or ID") + ret = append(ret, err) + } + if !(byEntityValid || byRelationValid) { + err := errors.New("relation has invalid FROM type and/or ID") + ret = append(ret, err) + } + } + return ret +} + +/* +Checks if a relation.ToType and relation.ToID are valid + Return: bool, whether the ToType and ToID are valid +*/ +func relationToValid(rel QueryRelationStruct, typeString string, minID int, maxID int) bool { + if rel.ToType == typeString && rel.ToID >= minID && rel.ToID <= maxID { + return true + } + return false +} + +/* +Checks if a relation.FromType and relation.FromID are valid + Return: bool, whether the FromType and FromID are valid +*/ +func relationFromValid(rel QueryRelationStruct, typeString string, minID int, maxID int) bool { + if rel.FromType == typeString && rel.FromID >= minID && rel.FromID <= maxID { + return true + } + return false +} + +func groupByGroupValid(groupBy QueryGroupByStruct, typeString string, minID int, maxID int) bool { + if groupBy.GroupType == typeString && groupBy.GroupID >= minID && groupBy.GroupID <= maxID { + return true + } + return false +} + +func groupByByValid(groupBy QueryGroupByStruct, typeString string, minID int, maxID int) bool { + if groupBy.ByType == typeString && groupBy.ByID >= minID && groupBy.ByID <= maxID { + return true + } + return false +} + +func getMinAndMaxEntityID(entities []QueryEntityStruct) (int, int) { + min := 65535 + max := -65535 + for _, e := range entities { + if e.ID < min { + min = e.ID + } + if e.ID > max { + max = e.ID + } + } + // If the min/max values didn't change the query would be invalid, and all consequent validationsteps will fail + return min, max +} + +func getMinAndMaxRelationID(relations []QueryRelationStruct) (int, int) { + min := 65535 + max := -65535 + for _, e := range relations { + if e.ID < min { + min = e.ID + } + if e.ID > max { + max = e.ID + } + } + // If the min/max values didn't change the query would be invalid, and all consequent validationsteps will fail + + return min, max +} + +func getMinAndMaxGroupByID(groupBys []QueryGroupByStruct) (int, int) { + min := 65535 + max := -65535 + for _, e := range groupBys { + if e.ID < min { + min = e.ID + } + if e.ID > max { + max = e.ID + } + } + // If the min/max values didn't change the query would be invalid, and all consequent validationsteps will fail + + return min, max +} diff --git a/interface.go b/interface.go index 2d7a76a82e7c27fc1ed327185b936ff33e38d4c8..e4a7500893ff7c9b4b18f260574eba79ec5e23d7 100644 --- a/interface.go +++ b/interface.go @@ -1,8 +1,14 @@ package query -import "git.science.uu.nl/graphpolaris/query-conversion/entity" +import ( + "git.science.uu.nl/graphpolaris/query-conversion/entity" + "git.science.uu.nl/graphpolaris/query-conversion/entity/entitycypher" +) // A Converter converts an incoming message in our JSON format to a format like AQL or Cypher type Converter interface { ConvertQuery(JSONQuery *entity.IncomingQueryJSON) (*string, error) } +type CypherConverterPlaceholder interface { + ConvertQuery(JSONQuery *entitycypher.IncomingQueryJSON) (*string, error) +} diff --git a/main/main.go b/main/main.go index 82d24ee2943c9a682d2e6751d946286bb4dcf54e..18a0f921aa06e4b0db2a9a4abf60d37bdf40bd4f 100644 --- a/main/main.go +++ b/main/main.go @@ -7,7 +7,9 @@ package main import ( "encoding/json" + "io/ioutil" "log" + "os" "git.science.uu.nl/graphpolaris/query-conversion/aql" "git.science.uu.nl/graphpolaris/query-conversion/entity" @@ -18,66 +20,16 @@ The main function that calls the appropriate functions */ func main() { queryservice := aql.NewService() - - js := []byte(`{ - "databaseName": "test", - "return": { - "entities": [ - 0, - 1, - 2, - 3 - ], - "relations": [ - 0, - 1 - ] - }, - "entities": [ - { - "type": "kamerleden", - "constraints": [] - }, - { - "type": "partijen", - "constraints": [] - } - , - { - "type": "kamerleden", - "constraints": [] - }, - { - "type": "commissies", - "constraints": [] - } - ], - "relations": [ - { - "type": "lid_van", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": 0, - "entityTo": 1, - "constraints": [] - }, - { - "type": "onderdeel_van", - "depth": { - "min": 1, - "max": 1 - }, - "entityFrom": 2, - "entityTo": 3, - "constraints": [] - } - ], - "limit": 5000, - "modifiers": [] - }`) - + jsonFile, err := os.Open("../realtest.json") + // if we os.Open returns an error then handle it + if err != nil { + log.Println(err) + } + log.Println("Successfully Opened users.json") + // defer the closing of our jsonFile so that we can parse it later on + defer jsonFile.Close() + js, _ := ioutil.ReadAll(jsonFile) + log.Println(string(js)) var inc entity.IncomingQueryJSON json.Unmarshal(js, &inc) result, _ := queryservice.ConvertQuery(&inc) diff --git a/realtest.json b/realtest.json new file mode 100644 index 0000000000000000000000000000000000000000..dbfd1cbc78a079835f78b6aedb58899a94bc23b3 --- /dev/null +++ b/realtest.json @@ -0,0 +1,42 @@ +{ + "databaseName": "TweedeKamer", + "return": { + "entities": [ + 0, + 1 + ], + "relations": [ + 0 + ] + }, + "entities": [ + { + "name": "parliament", + "ID": 0, + "constraints": [] + }, + { + "name": "parties", + "ID": 1, + "constraints": [] + } + ], + "relations": [ + { + "ID": 0, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": -4, + "constraints":[] + } + ], + "groupBys": [], + "limit": 5000, + "modifiers": [] +} \ No newline at end of file diff --git a/test.json b/test.json new file mode 100644 index 0000000000000000000000000000000000000000..a0a8ca6f562045486b8c4fcbd34dc5f1c63c2969 --- /dev/null +++ b/test.json @@ -0,0 +1,84 @@ +{ + "return": { + "entities": [ + 0, + 1, + 2 + ], + "relations": [ + 0, + 1 + ] + }, + "entities": [ + { + "ID": 0, + "name": "parliament" + }, + { + "ID": 1, + "name": "commissions" + }, + { + "ID": 2, + "name": "parliament" + } + ], + "relations": [ + { + "ID": 0, + "name": "part_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromId": 0, + "toType": "entity", + "toID": 1 + }, + { + "ID": 1, + "name": "part_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 2, + "toType": "groupBy", + "toID": 0 + } + ], + "groupBys": [ + { + "ID": 0, + "groupType": "entity", + "groupID": 0, + "groupAttribute": "age", + "byType": "entity", + "byID": 1, + "byAttribute": "name", + "appliedModifier": "AVG", + "relationID": 0 + } + ], + "filters": [ + { + "ID": 0, + "fromType": "groupBy", + "fromID": 0, + "toType": "relation", + "toID": 1, + "attribute": "age", + "value": "42", + "dataType": "number", + "matchType": "LT", + "inType": "", + "inID": 0 + } + ], + "limit": 5000, + "modifiers": [], + "databaseName": "TweedeKamer" +} \ No newline at end of file diff --git a/test2.json b/test2.json new file mode 100644 index 0000000000000000000000000000000000000000..48e9051faf3177dbabcac610eb59bdb9c7275322 --- /dev/null +++ b/test2.json @@ -0,0 +1,58 @@ +{ + "return": { + "entities": [ + 0, + 1, + 2 + ], + "relations": [ + 0, + 1 + ] + }, + "entities": [ + { + "ID": 0, + "name": "parties" + }, + { + "ID": 1, + "name": "parliament" + }, + { + "ID": 2, + "name": "commissions" + } + ], + "relations": [ + { + "ID": 0, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 1, + "toType": "entity", + "toID": 0 + }, + { + "ID": 1, + "name": "part_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 1, + "toType": "entity", + "toID": 2 + } + ], + "groupBys": [], + "filters": [], + "limit": 5000, + "modifiers": [], + "databaseName": "Tweede Kamer Dataset" +} \ No newline at end of file diff --git a/test3.json b/test3.json new file mode 100644 index 0000000000000000000000000000000000000000..93861fb8dff85a84dbfaeb3c02ef1de0410e29b4 --- /dev/null +++ b/test3.json @@ -0,0 +1,67 @@ +{ + "return": { + "entities": [ + 0, + 1 + ], + "relations": [ + 0 + ] + }, + "entities": [ + { + "ID": 0, + "name": "parliament" + }, + { + "ID": 1, + "name": "parties" + } + ], + "relations": [ + { + "ID": 0, + "name": "member_of", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 0, + "toType": "entity", + "toID": 1 + } + ], + "groupBys": [], + "filters": [ + { + "ID": 0, + "fromType": "entity", + "fromID": 0, + "toType": "filter", + "toID": 1, + "attribute": "age", + "value": "40", + "dataType": "number", + "matchType": "GT", + "inType": "", + "inID": 0 + }, + { + "ID": 1, + "fromType": "filter", + "fromID": 0, + "toType": "relation", + "toID": 0, + "attribute": "seniority", + "value": "3000", + "dataType": "number", + "matchType": "GT", + "inType": "", + "inID": 0 + } + ], + "limit": 5000, + "modifiers": [], + "databaseName": "Tweede Kamer Dataset" +} \ No newline at end of file