diff --git a/internal/usecases/convertquery/aql.go b/internal/usecases/convertquery/aql.go index 312a529511e0db8e757b8bd7637982ee48e2a233..ef6ff1eb597b4a709c8dcc1300c76f63193b063c 100644 --- a/internal/usecases/convertquery/aql.go +++ b/internal/usecases/convertquery/aql.go @@ -2,6 +2,7 @@ package convertquery import ( "encoding/json" + "errors" "fmt" ) @@ -19,6 +20,34 @@ func (s *Service) ConvertQuery(jsonMsg *[]byte) (*string, error) { return nil, err } + // Check to make sure all indexes exist + // How many entities are there + numEntities := len(jsonStruct.Entities) - 1 + // How many relations there are + numRelations := len(jsonStruct.Relations) - 1 + + // Make sure no entity should be returned that is outside the range of that list + for _, e := range jsonStruct.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 jsonStruct.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 jsonStruct.Return.Relations { + if r > numRelations || r < 0 { + return nil, errors.New("non-existing relation referenced in return") + } + } + result := createQuery(jsonStruct) return result, nil } @@ -66,13 +95,21 @@ func createQuery(jsonQuery *parsedJSON) *string { // 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) + + // Check if this entity index exists + if entity := &jsonQuery.Entities[relation.EntityFrom]; entity != nil { + ret += *createNodeLet(entity, &fromName) + } ret += *createRelationLetWithFromEntity(&relation, relationName, &jsonQuery.Entities, jsonQuery.Limit) } else if relation.EntityTo != -1 { // if there is only a to-node toName := fmt.Sprintf("n%v", relation.EntityTo) - ret += *createNodeLet(&jsonQuery.Entities[relation.EntityTo], &toName) + + // Check if this entity index exists + if entity := &jsonQuery.Entities[relation.EntityTo]; entity != nil { + ret += *createNodeLet(entity, &toName) + } ret += *createRelationLetWithOnlyToEntity(&relation, relationName, &jsonQuery.Entities, jsonQuery.Limit) // Add this relation to the list diff --git a/internal/usecases/convertquery/aql_test.go b/internal/usecases/convertquery/aql_test.go index 8df31686eb7b29a92b403d94ddc894294d19af08..d025bc5e743c49c1b1a5daa1994b8ccb36d81307 100644 --- a/internal/usecases/convertquery/aql_test.go +++ b/internal/usecases/convertquery/aql_test.go @@ -1,249 +1,552 @@ package convertquery import ( + "errors" + "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestMock(t *testing.T) { +func TestEmptyQueryConversion(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() - // s := `{"Return":{"Entities":[0,1],"Relations":[0]},"Entities":[{"Type":"airports","Constraints":[{"Attribute":"country","Value":"USA","DataType":"text","MatchType":"exact"}]},{"Type":"airports","Constraints":[{"Attribute":"city","Value":"New York","DataType":"text","MatchType":"exact"},{"Attribute":"vip","Value":"true","DataType":"bool","MatchType":"exact"}]}],"Relations":[{"Type":"flights","Depth":{"min":1,"max":1},"EntityFrom":0,"EntityTo":1,"Constraints":[{"Attribute":"Month","Value":"1","DataType":"number","MatchType":"exact"},{"Attribute":"Day","Value":"15","DataType":"number","MatchType":"exact"}]}]}` + query := []byte(`{ + "return": { + "entities": [], + "relations": [] + }, + "entities": [], + "relations": [], + "limit": 5000 + }`) - // s3 := []byte(s) + convertedResult, err := service.ConvertQuery(&query) - // // Convert the json byte msg to a query string - // convertQueryService := NewService() - // query, err := convertQueryService.ConvertQuery(&s3) - // if err != nil { - // errorhandler.LogError(err, "failed to parse incoming msg to query language") // TODO: send error message to client - // return - // } - // fmt.Println("Query: " + *query) + // Assert that there is no error + assert.NoError(t, err) - // // Make request to database - // // TODO : Generate database seperately - // // execute and retrieve result - // // convert result to general (node-link (?)) format - // requestService := request.NewService() - // result, err := requestService.SendAQLQuery(*query) - // if err != nil { - // logger.Log(err.Error()) - // return // TODO: Send message in queue notifying of error - // } + // Assert that the result and the expected result are the same + correctConvertedResult := ` +LET nodes = first(RETURN UNION_DISTINCT([],[])) +LET edges = first(RETURN UNION_DISTINCT([],[])) +RETURN {"vertices":nodes, "edges":edges }` + 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": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // 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 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) +} + +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": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": 0, + "entityTo": -1, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "number", + "matchType": "EQ" + } + ] + } + ], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // 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 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) +} + +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": "text", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 3 + }, + "entityFrom": 1, + "entityTo": 0, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "number", + "matchType": "EQ" + } + ] + } + ], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // Assert that there is no error + 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) +} + +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": "text", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 3 + }, + "entityFrom": 2, + "entityTo": 1, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "number", + "matchType": "EQ" + } + ] + }, + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": 0, + "entityTo": -1, + "constraints": [] + } + ], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // Assert that there is no error + 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) +} + +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": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -1, + "entityTo": 0, + "constraints": [] + } + ], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // 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 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) +} + +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": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -1, + "entityTo": 0, + "constraints": [] + } + ], + "limit": 5000 + }`) + + _, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.Equal(t, errors.New("non-existing entity referenced in return"), err) +} - // fmt.Print("QueryResult: ") - // fmt.Println(*result) +func TestTooManyReturnRelations(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() - assert.True(t, true, true) + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [ + 0, + 1, + 2 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -1, + "entityTo": 0, + "constraints": [] + } + ], + "limit": 5000 + }`) + + _, err := service.ConvertQuery(&query) + + // 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": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -1, + "entityTo": 0, + "constraints": [] + } + ], + "limit": 5000 + }`) + + _, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.Equal(t, errors.New("non-existing entity referenced in return"), err) } -// func TestHugeQuery(t *testing.T) { - -// s := `{ -// "Return": { -// "Entities": [ -// 0, -// 1 -// ], -// "Relations": [ -// 0 -// ] -// }, -// "Entities": [ -// { -// "Type": "airports", -// "Constraints": [ -// { -// "Attribute": "country", -// "Value": "USA", -// "DataType": "text", -// "MatchType": "exact" -// } -// ] -// }, -// { -// "Type": "airports", -// "Constraints": [ -// { -// "Attribute": "city", -// "Value": "New York", -// "DataType": "text", -// "MatchType": "exact" -// }, -// { -// "Attribute": "vip", -// "Value": "true", -// "DataType": "bool", -// "MatchType": "exact" -// } -// ] -// } -// ], -// "Relations": [ -// { -// "Type": "flights", -// "Depth": { -// "min": 1, -// "max": 1 -// }, -// "EntityFrom": 0, -// "EntityTo": 1, -// "Constraints": [ -// { -// "Attribute": "Month", -// "Value": "1", -// "DataType": "number", -// "MatchType": "exact" -// }, -// { -// "Attribute": "Day", -// "Value": "15", -// "DataType": "number", -// "MatchType": "exact" -// } -// ] -// } -// ] -// }` - -// s3 := []byte(s) -// convertQueryService := NewService() -// j, _ := convertQueryService.ConvertQuery(&s3) - -// expected := `LET n0 = ( -// FOR x IN airports -// FILTER x.country == "USA" -// RETURN x -// ) -// LET r0 = ( -// FOR x IN n0 -// FOR v, e, p IN 1..1 OUTBOUND x flights -// OPTIONS { uniqueEdges: "path" } -// FILTER v.city == "New York" -// AND v.vip == true -// FILTER p.edges[*].Month ALL == 1 -// AND p.edges[*].Day ALL == 15 -// LIMIT 1000 -// 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 }` - -// assert.Equal(t, *j, expected) -// } -// func TestOnlyEntitiesQuery(t *testing.T) { - -// s := `{ -// "Return": { -// "Entities": [ -// 0 -// ], -// "Relations": [] -// }, -// "Entities": [ -// { -// "Type": "airports", -// "Constraints": [ -// { -// "Attribute": "city", -// "Value": "New York", -// "DataType": "text", -// "MatchType": "exact" -// }, -// { -// "Attribute": "country", -// "Value": "USA", -// "DataType": "text", -// "MatchType": "exact" -// } -// ] -// } -// ], -// "Relations": [] -// }` - -// s3 := []byte(s) -// convertQueryService := NewService() -// j, _ := convertQueryService.ConvertQuery(&s3) - -// expected := `LET n0 = ( -// FOR x IN airports -// FILTER x.city == "New York" -// AND x.country == "USA" -// RETURN x -// ) - -// LET nodes = first(RETURN UNION_DISTINCT(n0,[],[])) -// LET edges = first(RETURN UNION_DISTINCT([],[])) -// RETURN {"vertices":nodes, "edges":edges }` - -// assert.Equal(t, expected, *j) -// } -// func TestInboundQuery(t *testing.T) { - -// s := `{ -// "Return": { -// "Entities": [ -// 0 -// ], -// "Relations": [ -// 0 -// ] -// }, -// "Entities": [ -// { -// "Type": "airports", -// "Constraints": [ -// { -// "Attribute": "city", -// "Value": "New York", -// "DataType": "text", -// "MatchType": "exact" -// } -// ] -// } -// ], -// "Relations": [ -// { -// "Type": "flights", -// "Depth": { -// "min": 1, -// "max": 1 -// }, -// "EntityFrom": -1, -// "EntityTo": 0, -// "Constraints": [{ -// "Attribute": "Day", -// "Value": "15", -// "DataType": "number", -// "MatchType": "exact" -// }] -// } -// ] -// }` - -// s3 := []byte(s) -// convertQueryService := NewService() -// j, _ := convertQueryService.ConvertQuery(&s3) - -// expected := `LET n0 = ( -// FOR x IN airports -// FILTER x.city == "New York" -// RETURN x -// ) -// LET r0 = ( -// FOR x IN n0 -// FOR v, e, p IN 1..1 INBOUND x flights -// OPTIONS { uniqueEdges: "path" } -// FILTER p.edges[*].Day ALL == 15 -// LIMIT 1000 -// 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 }` - -// assert.Equal(t, expected, *j) -// } +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": "text", + "matchType": "exact" + } + ] + } + ], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // 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) +}