diff --git a/aql/convertQueryBenchmark_test.go b/aql/convertQueryBenchmark_test.go index b1ba7b7b6537c0a9ce571c60e5698b73967fa223..6d608cd8499b29b72913f4b9ff8d785dcbcc0b98 100644 --- a/aql/convertQueryBenchmark_test.go +++ b/aql/convertQueryBenchmark_test.go @@ -52,7 +52,7 @@ func BenchmarkConvertOneAttributeQuery(b *testing.B) { { "attribute": "state", "value": "HI", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -97,7 +97,7 @@ func BenchmarkConvertTwoRelationQuery(b *testing.B) { { "attribute": "city", "value": "New York", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -108,7 +108,7 @@ func BenchmarkConvertTwoRelationQuery(b *testing.B) { { "attribute": "city", "value": "San Francisco", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -119,7 +119,7 @@ func BenchmarkConvertTwoRelationQuery(b *testing.B) { { "attribute": "state", "value": "HI", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -138,7 +138,7 @@ func BenchmarkConvertTwoRelationQuery(b *testing.B) { { "attribute": "Day", "value": "15", - "dataType": "number", + "dataType": "int", "matchType": "EQ" } ] diff --git a/aql/convertQuery_test.go b/aql/convertQuery_test.go index 44917c8ff658f9c43ec182cd9653c26fc99ba8f5..0ab5490f6220eb3e2fde0db74f70d9f7d347c142 100644 --- a/aql/convertQuery_test.go +++ b/aql/convertQuery_test.go @@ -135,7 +135,7 @@ func TestEntityOneAttributeQuery(t *testing.T) { { "attribute": "state", "value": "HI", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -182,7 +182,7 @@ func TestRelationWithConstraint(t *testing.T) { { "attribute": "state", "value": "HI", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -201,7 +201,7 @@ func TestRelationWithConstraint(t *testing.T) { { "attribute": "Day", "value": "15", - "dataType": "number", + "dataType": "int", "matchType": "EQ" } ] @@ -245,7 +245,7 @@ func TestModifierCountEntity(t *testing.T) { { "attribute": "state", "value": "HI", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -297,7 +297,7 @@ func TestModifierCountEntityAttribute(t *testing.T) { { "attribute": "state", "value": "HI", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -351,7 +351,7 @@ func TestModifierCountRelation(t *testing.T) { { "attribute": "state", "value": "HI", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -370,7 +370,7 @@ func TestModifierCountRelation(t *testing.T) { { "attribute": "Day", "value": "15", - "dataType": "number", + "dataType": "int", "matchType": "EQ" } ] @@ -486,7 +486,7 @@ func TestModifierCountRelationAttribute(t *testing.T) { { "attribute": "state", "value": "HI", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -505,7 +505,7 @@ func TestModifierCountRelationAttribute(t *testing.T) { { "attribute": "Day", "value": "15", - "dataType": "number", + "dataType": "int", "matchType": "EQ" } ] @@ -560,7 +560,7 @@ func TestRelationWithInOutConstraint(t *testing.T) { { "attribute": "city", "value": "San Francisco", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -571,7 +571,7 @@ func TestRelationWithInOutConstraint(t *testing.T) { { "attribute": "state", "value": "HI", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -590,7 +590,7 @@ func TestRelationWithInOutConstraint(t *testing.T) { { "attribute": "Day", "value": "15", - "dataType": "number", + "dataType": "int", "matchType": "EQ" } ] @@ -639,7 +639,7 @@ func TestTwoRelations(t *testing.T) { { "attribute": "city", "value": "New York", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -650,7 +650,7 @@ func TestTwoRelations(t *testing.T) { { "attribute": "city", "value": "San Francisco", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -661,7 +661,7 @@ func TestTwoRelations(t *testing.T) { { "attribute": "state", "value": "HI", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -680,7 +680,7 @@ func TestTwoRelations(t *testing.T) { { "attribute": "Day", "value": "15", - "dataType": "number", + "dataType": "int", "matchType": "EQ" } ] @@ -736,7 +736,7 @@ func TestRelationWithOnlyToNode(t *testing.T) { { "attribute": "city", "value": "San Francisco", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -796,7 +796,7 @@ func TestTooManyReturnEntities(t *testing.T) { { "attribute": "city", "value": "San Francisco", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -850,7 +850,7 @@ func TestTooManyReturnRelations(t *testing.T) { { "attribute": "city", "value": "San Francisco", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -905,7 +905,7 @@ func TestNegativeReturnEntities(t *testing.T) { { "attribute": "city", "value": "San Francisco", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -954,7 +954,7 @@ func TestNoRelationsField(t *testing.T) { { "attribute": "city", "value": "San Francisco", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] @@ -1000,7 +1000,7 @@ func TestEntityFromLowerThanNegativeOneInRelation(t *testing.T) { { "attribute": "city", "value": "San Francisco", - "dataType": "text", + "dataType": "string", "matchType": "exact" } ] diff --git a/aql/createConstraints.go b/aql/createConstraints.go index cdc9891d56705ae75fbac1d28b96a95eac7799d6..0a6c8ed08fca24bbe593323320a6b7b505fcb605 100644 --- a/aql/createConstraints.go +++ b/aql/createConstraints.go @@ -46,12 +46,12 @@ func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, na ) // Constraint datatypes back end - // text MatchTypes: EQ/NEQ/contains/excludes - // number MatchTypes: EQ/NEQ/GT/LT/GET/LET + // string MatchTypes: EQ/NEQ/contains/excludes + // int MatchTypes: EQ/NEQ/GT/LT/GET/LET // bool MatchTypes: EQ/NEQ switch constraint.DataType { - case "text": + case "string": value = fmt.Sprintf("\"%s\"", constraint.Value) switch constraint.MatchType { case "NEQ": @@ -65,7 +65,7 @@ func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, na default: //EQ match = "==" } - case "number": + case "int": value = constraint.Value switch constraint.MatchType { case "NEQ": diff --git a/cypher/convertQuery.go b/cypher/convertQuery.go new file mode 100644 index 0000000000000000000000000000000000000000..cd869cb02ed437dc45ad2a3deed65edf6b00fd98 --- /dev/null +++ b/cypher/convertQuery.go @@ -0,0 +1,229 @@ +package cypher + +import ( + "errors" + "fmt" + "strings" + + "git.science.uu.nl/datastrophe/query-conversion/entity" +) + +/* +ConvertQuery converts an IncomingQueryJSON object into AQL + JSONQuery: *entity.IncomingQueryJSON, the query to be converted to AQL + Returns: *string, the AQL query and a possible error +*/ +func (s *Service) ConvertQuery(JSONQuery *entity.IncomingQueryJSON) (*string, error) { + + // Check to make sure all indexes exist + // How many entities are there + numEntities := len(JSONQuery.Entities) - 1 + // How many relations there are + numRelations := len(JSONQuery.Relations) - 1 + + // Make sure no entity should be returned that is outside the range of that list + for _, e := range JSONQuery.Return.Entities { + // If this entity references an entity that is outside the range + if e > numEntities || e < 0 { + return nil, errors.New("non-existing entity referenced in return") + } + } + + // Make sure that no relation mentions a non-existing entity + for _, r := range JSONQuery.Relations { + if r.EntityFrom > numEntities || r.EntityTo > numEntities { + return nil, errors.New("non-exisiting entity referenced in relation") + } + } + + // Make sure no non-existing relation is tried to be returned + for _, r := range JSONQuery.Return.Relations { + if r > numRelations || r < 0 { + return nil, errors.New("non-existing relation referenced in return") + } + } + + result := createQuery(JSONQuery) + return result, nil +} + +func sliceContains(s []int, e int) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +/*TrimSuffix trims the final character of a string */ +func TrimSuffix(s, suffix string) string { + if strings.HasSuffix(s, suffix) { + s = s[:len(s)-len(suffix)] + } + return s +} + +/* createQuery generates a query based on the json file provided +Parameters: jsonQuery is a parsedJSON struct holding all the data needed to form a query + +Return: a string containing the corresponding AQL query and an error +*/ +func createQuery(JSONQuery *entity.IncomingQueryJSON) *string { + // Note: Case #4, where there is an edge only query (without any entity), is not supported by frontend + + // If a modifier is used, disable the limit + if len(JSONQuery.Modifiers) > 0 { + JSONQuery.Limit = -1 + } + + var ( + relationsToReturn []string + nodesToReturn []string + nodeUnion string + relationUnion string + queryList [][][]int + entityList []int + ret string + ) + + for i, relation := range JSONQuery.Relations { + var contains bool + contains = false + for j := range queryList { + if sliceContains(queryList[j][0], relation.EntityFrom) || sliceContains(queryList[j][0], relation.EntityTo) { + if !sliceContains(queryList[j][0], relation.EntityFrom) { + queryList[j][0] = append(queryList[j][0], relation.EntityFrom) + entityList = append(entityList, relation.EntityFrom) + } + if !sliceContains(queryList[j][0], relation.EntityTo) { + queryList[j][0] = append(queryList[j][0], relation.EntityTo) + entityList = append(entityList, relation.EntityTo) + } + queryList[j][1] = append(queryList[j][1], i) + contains = true + } + } + if !contains { + queryList = append(queryList, [][]int{{relation.EntityFrom, relation.EntityTo}, {i}}) + } + } + + for i := range queryList { + //reset variables for the next query + nodeUnion = "" + relationUnion = "" + relationsToReturn = []string{} + for j, relationID := range queryList[i][1] { + relationName := fmt.Sprintf("r%v", j) + relation := JSONQuery.Relations[relationID] + pathName := fmt.Sprintf("p%v", j) + relationsToReturn = append(relationsToReturn, pathName) + if relation.EntityFrom >= 0 { + // if there is a from-node + // create the let for this node + fromName := fmt.Sprintf("n%v", relation.EntityFrom) + + ret += *createNodeMatch(&JSONQuery.Entities[relation.EntityFrom], &fromName) + + ret += *createRelationMatch(&relation, relationName, pathName, &JSONQuery.Entities, JSONQuery.Limit, true) + } else if relation.EntityTo >= 0 { + // if there is only a to-node + toName := fmt.Sprintf("n%v", relation.EntityTo) + + ret += *createNodeMatch(&JSONQuery.Entities[relation.EntityTo], &toName) + + ret += *createRelationMatch(&relation, relationName, pathName, &JSONQuery.Entities, JSONQuery.Limit, false) + // Add this relation to the list + } else { + fmt.Println("Relation-only queries are currently not supported") + continue + } + } + + // Create UNION statements that create unique lists of all the nodes and relations + + // Thus removing all duplicates + nodeUnion = "RETURN " + + for _, entityID := range queryList[i][0] { + if sliceContains(JSONQuery.Return.Entities, entityID) { + nodeUnion += fmt.Sprintf("n%v,", entityID) + } + } + + for _, relation := range relationsToReturn { + relationUnion += fmt.Sprintf("%v,", relation) + } + + relationUnion = TrimSuffix(relationUnion, ",") + // hier zat een newline + ret += nodeUnion + relationUnion + "; " + } + + nodeSet := make(map[int]bool) + for _, relation := range JSONQuery.Relations { + nodeSet[relation.EntityFrom] = true + nodeSet[relation.EntityTo] = true + } + + // Check if the entities to return are already returned + for _, entityIndex := range JSONQuery.Return.Entities { + if !nodeSet[entityIndex] { + // If not, return this node + name := fmt.Sprintf("n%v", entityIndex) + ret += *createNodeMatch(&JSONQuery.Entities[entityIndex], &name) + // Add this node to the list + nodesToReturn = append(nodesToReturn, name) + ret += fmt.Sprintf("RETURN %v", name) + } + } + + ret = TrimSuffix(ret, " ") + return &ret +} + +/* createNodeLet generates a 'LET' statement for a node related query +Parameters: node is an entityStruct containing the information of a single node, +name is the autogenerated name of the node consisting of "n" + the index of the node + +Return: a string containing a single LET-statement in AQL +*/ +func createNodeMatch(node *entity.QueryEntityStruct, name *string) *string { + // hier zat een newline + header := fmt.Sprintf("MATCH (%v:%v) ", *name, node.Type) + constraints := *createConstraintStatements(&node.Constraints, *name) + ret := header + constraints + return &ret +} + +/* createRelationLetWithFromEntity generates a 'LET' statement for relations with an 'EntityFrom' property and optionally an 'EntitiyTo' property +Parameters: relation is a relation struct containing the information of a single relation, +name is the autogenerated name of the node consisting of "r" + the index of the relation, +entities is a list of entityStructs that are needed to form the relation LET-statement + +Return: a string containing a single LET-statement in AQL +*/ +func createRelationMatch(relation *entity.QueryRelationStruct, relationName string, pathName string, entities *[]entity.QueryEntityStruct, limit int, outbound bool) *string { + relationReturn := "" + var relationBounds int + if outbound { + relationReturn = fmt.Sprintf("MATCH %v = (n%v)-[%v:%v*%v..%v]->(", pathName, relation.EntityFrom, relationName, relation.Type, relation.Depth.Min, relation.Depth.Max) + relationBounds = relation.EntityTo + + } else { + relationReturn = fmt.Sprintf("MATCH %v = (n%v)-[%v:%v*%v..%v]->(", pathName, relation.EntityTo, relationName, relation.Type, relation.Depth.Min, relation.Depth.Max) + relationBounds = relation.EntityFrom + } + + if relationBounds != -1 { + relationReturn += fmt.Sprintf("n%v", relationBounds) + } + relationReturn += ")" + + constraintReturn := *createConstraintStatements(&relation.Constraints, relationName) + // hier zat een newline + ret := relationReturn + " " + constraintReturn + + return &ret +} diff --git a/cypher/convertQueryBenchmark_test.go b/cypher/convertQueryBenchmark_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3794ab92a7434af21c8fcc34fb640a6d1d789f3a --- /dev/null +++ b/cypher/convertQueryBenchmark_test.go @@ -0,0 +1,169 @@ +package cypher + +import ( + "encoding/json" + "testing" + + "git.science.uu.nl/datastrophe/query-conversion/entity" +) + +func BenchmarkConvertEmptyQuery(b *testing.B) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [], + "relations": [] + }, + "entities": [], + "relations": [], + "limit": 5000 + }`) + + // Unmarshall the incoming message into an IncomingJSONQuery object + var JSONQuery entity.IncomingQueryJSON + json.Unmarshal(query, &JSONQuery) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + service.ConvertQuery(&JSONQuery) + } +} + +func BenchmarkConvertOneAttributeQuery(b *testing.B) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "string", + "matchType": "exact" + } + ] + } + ], + "relations": [], + "limit": 5000 + }`) + + // Unmarshall the incoming message into an IncomingJSONQuery object + var JSONQuery entity.IncomingQueryJSON + json.Unmarshal(query, &JSONQuery) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + service.ConvertQuery(&JSONQuery) + } +} + +func BenchmarkConvertTwoRelationQuery(b *testing.B) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0, + 1, + 2 + ], + "relations": [ + 0, + 1 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "New York", + "dataType": "string", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "string", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "string", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 3 + }, + "entityFrom": 2, + "entityTo": 1, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "int", + "matchType": "EQ" + } + ] + }, + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": 0, + "entityTo": -1, + "constraints": [] + } + ], + "limit": 5000 + }`) + + // Unmarshall the incoming message into an IncomingJSONQuery object + var JSONQuery entity.IncomingQueryJSON + json.Unmarshal(query, &JSONQuery) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + service.ConvertQuery(&JSONQuery) + } +} diff --git a/cypher/convertQuery_test.go b/cypher/convertQuery_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c07a06556a6e16f92d52ffae50e7de21d0d75a70 --- /dev/null +++ b/cypher/convertQuery_test.go @@ -0,0 +1,894 @@ +package cypher + +import ( + "encoding/json" + "errors" + "strings" + "testing" + + "git.science.uu.nl/datastrophe/query-conversion/entity" + "github.com/stretchr/testify/assert" +) + +func TestEmptyQueryConversion(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [], + "relations": [] + }, + "entities": [], + "relations": [], + "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 := `` + assert.Equal(t, correctConvertedResult, *convertedResult) +} + +func TestEntityOneAttributeQuery(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "string", + "matchType": "exact" + } + ] + } + ], + "relations": [], + "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 := `MATCH (n0:airports)WHERE n0.state = "HI" RETURN n0` + cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") + cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") + assert.Equal(t, correctConvertedResult, cleanedResult) +} + +func TestRelationWithConstraint(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [ + 0 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "string", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": 0, + "entityTo": -1, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "int", + "matchType": "EQ" + } + ] + } + ], + "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 := `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) +} + +// func TestModifierCountEntity(t *testing.T) { +// // Setup for test +// // Create query conversion service +// service := NewService() + +// query := []byte(`{ +// "return": { +// "entities": [ +// 0 +// ], +// "relations": [] +// }, +// "entities": [ +// { +// "type": "airports", +// "constraints": [ +// { +// "attribute": "state", +// "value": "HI", +// "dataType": "string", +// "matchType": "exact" +// } +// ] +// } +// ], +// "relations": [], +// "limit": 5000, +// "modifiers": [ +// { +// "type": "COUNT", +// "selectedType": "entity", +// "id": 0, +// "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 := `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) +// } + +// func TestModifierCountEntityAttribute(t *testing.T) { +// // Setup for test +// // Create query conversion service +// service := NewService() + +// query := []byte(`{ +// "return": { +// "entities": [ +// 0 +// ], +// "relations": [] +// }, +// "entities": [ +// { +// "type": "airports", +// "constraints": [ +// { +// "attribute": "state", +// "value": "HI", +// "dataType": "string", +// "matchType": "exact" +// } +// ] +// } +// ], +// "relations": [], +// "limit": 5000, +// "modifiers": [ +// { +// "type": "SUM", +// "selectedType": "entity", +// "id": 0, +// "attributeIndex": 0 +// } +// ] +// }`) + +// // 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.state == "HI" RETURN x)RETURN SUM (n0[*].state)` +// cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") +// cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") +// assert.Equal(t, correctConvertedResult, cleanedResult) +// } + +// func TestModifierCountRelation(t *testing.T) { +// // Setup for test +// // Create query conversion service +// service := NewService() + +// query := []byte(`{ +// "return": { +// "entities": [ +// 0 +// ], +// "relations": [ +// 0 +// ] +// }, +// "entities": [ +// { +// "type": "airports", +// "constraints": [ +// { +// "attribute": "state", +// "value": "HI", +// "dataType": "string", +// "matchType": "exact" +// } +// ] +// } +// ], +// "relations": [ +// { +// "type": "flights", +// "depth": { +// "min": 1, +// "max": 1 +// }, +// "entityFrom": 0, +// "entityTo": -1, +// "constraints": [ +// { +// "attribute": "Day", +// "value": "15", +// "dataType": "int", +// "matchType": "EQ" +// } +// ] +// } +// ], +// "limit": 5000, +// "modifiers": [ +// { +// "type": "COUNT", +// "selectedType": "relation", +// "id": 0, +// "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 := `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) +// } +// func TestModifierCountRelationAttribute(t *testing.T) { +// // Setup for test +// // Create query conversion service +// service := NewService() + +// query := []byte(`{ +// "return": { +// "entities": [ +// 0 +// ], +// "relations": [ +// 0 +// ] +// }, +// "entities": [ +// { +// "type": "airports", +// "constraints": [ +// { +// "attribute": "state", +// "value": "HI", +// "dataType": "string", +// "matchType": "exact" +// } +// ] +// } +// ], +// "relations": [ +// { +// "type": "flights", +// "depth": { +// "min": 1, +// "max": 1 +// }, +// "entityFrom": 0, +// "entityTo": -1, +// "constraints": [ +// { +// "attribute": "Day", +// "value": "15", +// "dataType": "int", +// "matchType": "EQ" +// } +// ] +// } +// ], +// "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) + +// 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.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) +// } + +func TestRelationWithInOutConstraint(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0, + 1 + ], + "relations": [ + 0 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "string", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "string", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 3 + }, + "entityFrom": 1, + "entityTo": 0, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "int", + "matchType": "EQ" + } + ] + } + ], + "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 := `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) +} + +func TestTwoRelations(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0, + 1, + 2 + ], + "relations": [ + 0, + 1 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "New York", + "dataType": "string", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "string", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "string", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 3 + }, + "entityFrom": 2, + "entityTo": 1, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "int", + "matchType": "EQ" + } + ] + }, + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": 0, + "entityTo": -1, + "constraints": [] + } + ], + "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 := `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) +} + +func TestRelationWithOnlyToNode(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "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) + + 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 := `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) +} + +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) +} + +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) +} + +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) +} + +func TestNoRelationsField(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ] + }, + "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 := `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) +} + +func TestEntityFromLowerThanNegativeOneInRelation(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [ + 0 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "string", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -4, + "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.NoError(t, err) +} diff --git a/cypher/createConstraints.go b/cypher/createConstraints.go new file mode 100644 index 0000000000000000000000000000000000000000..b24d895f7c9afbccd519bc4c6ec3c9fecd89de9a --- /dev/null +++ b/cypher/createConstraints.go @@ -0,0 +1,101 @@ +package cypher + +import ( + "fmt" + + "git.science.uu.nl/datastrophe/query-conversion/entity" +) + +/* createConstraintStatements generates the appropriate amount of constraint lines calling createConstraingBoolExpression +Parameters: constraints is a list of constraintStructs that specify the constraints of a node or relation, +name is the id of the corresponding relation/node, +isRelation is a boolean specifying if this constraint comes from a node or relation + +Return: a string containing a FILTER-statement with all the constraints +*/ +func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, name string) *string { + s := "" + if len(*constraints) == 0 { + return &s + } + + newLineStatement := "\tWHERE" + + for _, v := range *constraints { + s += fmt.Sprintf("%v%v \n", newLineStatement, *createConstraintBoolExpression(&v, name)) + newLineStatement = "\tAND" + } + + return &s +} + +/* createConstraintBoolExpression generates a single boolean expression, +e.g. {name}.city == "New York". + +Parameters: constraint is a single constraint of a node or relation, +name is the id of the corresponding relation/node, +isRelation is a boolean specifying if this constraint comes from a node or relation, that changes the structure of the expression + +Return: a string containing an boolean expression of a single constraint +*/ +func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, name string) *string { + var ( + match string + value string + line string + neq string + ) + + // Constraint datatypes back end + // string MatchTypes: EQ/NEQ/contains/excludes + // int MatchTypes: EQ/NEQ/GT/LT/GET/LET + // bool MatchTypes: EQ/NEQ + + neq = "" + + switch constraint.DataType { + case "string": + value = fmt.Sprintf("\"%s\"", constraint.Value) + switch constraint.MatchType { + case "NEQ": + match = "<>" + case "contains": + match = "CONTAINS" + value = fmt.Sprintf("\"%%%s%%\"", constraint.Value) + case "excludes": + match = "CONTAINS" + value = fmt.Sprintf("\"%%%s%%\"", constraint.Value) + neq = "NOT" + default: //EQ + match = "=" + } + case "int": + value = constraint.Value + switch constraint.MatchType { + case "NEQ": + match = "<>" + case "GT": + match = ">" + case "LT": + match = "<" + case "GET": + match = ">=" + case "LET": + match = "<=" + default: //EQ + match = "=" + } + default: /*bool*/ + value = constraint.Value + switch constraint.MatchType { + case "NEQ": + match = "<>" + default: //EQ + match = "=" + } + } + + line = fmt.Sprintf("%s %s.%s %s %s", neq, name, constraint.Attribute, match, value) + + return &line +} diff --git a/cypher/queryConverter.go b/cypher/queryConverter.go new file mode 100644 index 0000000000000000000000000000000000000000..1b0536f033d0d218777d633f84d0f67664b7fbb0 --- /dev/null +++ b/cypher/queryConverter.go @@ -0,0 +1,10 @@ +package cypher + +// Service implements the QueryConverter interface (in the query service) +type Service struct { +} + +// NewService creates a new AQL conversion service +func NewService() *Service { + return &Service{} +} diff --git a/entity/queryStruct.go b/entity/queryStruct.go index 9dd51dcc1e3a1c5a71a890483ca37175bb16502d..7b6c2760ac2ad7c0b1a002c516559011a85cdd5d 100644 --- a/entity/queryStruct.go +++ b/entity/queryStruct.go @@ -49,8 +49,8 @@ type QuerySearchDepthStruct struct { // QueryConstraintStruct holds the information of the constraint // Constraint datatypes -// text MatchTypes: exact/contains/startswith/endswith -// number MatchTypes: GT/LT/EQ +// string MatchTypes: exact/contains/startswith/endswith +// int MatchTypes: GT/LT/EQ // bool MatchTypes: EQ/NEQ type QueryConstraintStruct struct { Attribute string diff --git a/main/main.go b/main/main.go new file mode 100644 index 0000000000000000000000000000000000000000..dfb886b2e6cd140bbd06d38f3f6ea4ddd9b7471c --- /dev/null +++ b/main/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "encoding/json" + "log" + + "git.science.uu.nl/datastrophe/query-conversion/cypher" + "git.science.uu.nl/datastrophe/query-conversion/entity" +) + +func main() { + queryservice := cypher.NewService() + + js := []byte(`{ + "return": { + "entities": [ + 0 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "string", + "matchType": "exact" + } + ] + } + ], + "limit": 5000 + }`) + + var inc entity.IncomingQueryJSON + json.Unmarshal(js, &inc) + result, _ := queryservice.ConvertQuery(&inc) + log.Println(*result) +} diff --git a/main/node_modules/.yarn-integrity b/main/node_modules/.yarn-integrity new file mode 100644 index 0000000000000000000000000000000000000000..1a3aded6105fb67caa747e15081dd1543f4cb74b --- /dev/null +++ b/main/node_modules/.yarn-integrity @@ -0,0 +1,10 @@ +{ + "systemParams": "win32-x64-72", + "modulesFolders": [], + "flags": [], + "linkedModules": [], + "topLevelPatterns": [], + "lockfileEntries": {}, + "files": [], + "artifacts": {} +} \ No newline at end of file diff --git a/main/yarn.lock b/main/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..fb57ccd13afbd082ad82051c2ffebef4840661ec --- /dev/null +++ b/main/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +