diff --git a/convertQuery.go b/convertQuery.go new file mode 100644 index 0000000000000000000000000000000000000000..4c5420a7525aabef852f3c1ea4a0e179ccbad3d8 --- /dev/null +++ b/convertQuery.go @@ -0,0 +1,322 @@ +package aql + +import ( + "encoding/json" + "errors" + "fmt" + + "git.science.uu.nl/datastrophe/aql-conversion/entity" +) + +/* +ConvertQuery converts a json string to an AQL query + +Parameters: jsonMsg is the JSON file directly outputted by the drag and drop query builder in the frontend + +Return: a string containing the corresponding AQL query, a string containing the database name and an error */ +func (s *Service) ConvertQuery(jsonMsg *[]byte) (*string, *string, error) { + + jsonStruct, err := convertJSONToStruct(jsonMsg) + if err != nil { + return nil, 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, 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, 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, nil, errors.New("non-existing relation referenced in return") + } + } + + result := createQuery(jsonStruct) + return result, &jsonStruct.DatabaseName, nil +} + +/* convertJSONtoStruct reads a JSON file and sorts the data into the appropriate structs +Parameters: jsonMsg is the JSON file directly outputted by the drag and drop query builder in the frontend + +Return: parsedJSON is a struct with the same structure and holding the same data as jsonMsg +*/ +func convertJSONToStruct(jsonMsg *[]byte) (*entity.QueryParsedJSON, error) { + jsonStruct := entity.QueryParsedJSON{} + err := json.Unmarshal(*jsonMsg, &jsonStruct) + + if err != nil { + return nil, err + } + + return &jsonStruct, nil +} + +/* createQuery generates a query based on the json file provided +Parameters: jsonQuery is a parsedJSON struct holding all the data needed to form a query + +Return: a string containing the corresponding AQL query and an error +*/ +func createQuery(jsonQuery *entity.QueryParsedJSON) *string { + // Note: Case #4, where there is an edge only query (without any entity), is not supported by frontend + + // If a modifier is used, disable the limit + if len(jsonQuery.Modifiers) > 0 { + jsonQuery.Limit = -1 + } + + var ( + relationsToReturn []string + nodesToReturn []string + nodeUnion string + relationUnion string + ) + + // Loop over all relations + ret := "" + + for i, relation := range jsonQuery.Relations { + + relationName := fmt.Sprintf("r%v", i) + + if relation.EntityFrom >= 0 { + // if there is a from-node + // create the let for this node + fromName := fmt.Sprintf("n%v", relation.EntityFrom) + + ret += *createNodeLet(&jsonQuery.Entities[relation.EntityFrom], &fromName) + + ret += *createRelationLetWithFromEntity(&relation, relationName, &jsonQuery.Entities, jsonQuery.Limit) + } else if relation.EntityTo >= 0 { + // if there is only a to-node + toName := fmt.Sprintf("n%v", relation.EntityTo) + + ret += *createNodeLet(&jsonQuery.Entities[relation.EntityTo], &toName) + + ret += *createRelationLetWithOnlyToEntity(&relation, relationName, &jsonQuery.Entities, jsonQuery.Limit) + // Add this relation to the list + } else { + fmt.Println("Relation-only queries are currently not supported") + continue + } + + // Add this relation to the list + relationsToReturn = append(relationsToReturn, relationName) + } + + // Add node let statements for nodes that are not yet returned + // Create a set from all the entity-from's and entity-to's, to check if they are returned + nodeSet := make(map[int]bool) + for _, relation := range jsonQuery.Relations { + nodeSet[relation.EntityFrom] = true + nodeSet[relation.EntityTo] = true + } + + // Check if the entities to return are already returned + for _, entityIndex := range jsonQuery.Return.Entities { + if !nodeSet[entityIndex] { + // If not, return this node + name := fmt.Sprintf("n%v", entityIndex) + ret += *createNodeLet(&jsonQuery.Entities[entityIndex], &name) + + // Add this node to the list + nodesToReturn = append(nodesToReturn, name) + } + } + + //If there are modifiers within the query, we run a different set of checks which focus on quantifiable aspects + if len(jsonQuery.Modifiers) > 0 { + modifier := jsonQuery.Modifiers[0] + // There is a distinction between (relations and entities) and (relations or entities) + if len(jsonQuery.Return.Relations) > 0 && len(jsonQuery.Return.Entities) > 0 { + + var pathDistinction string // .vertices or .edges + + // Select the correct addition to the return of r0[**] + if modifier.SelectedType == "entity" { + // ASSUMING THERE IS ONLY 1 RELATION + if jsonQuery.Relations[0].EntityFrom == modifier.ID { + pathDistinction = fmt.Sprintf(".vertices[%v]", jsonQuery.Relations[0].Depth.Min-1) + + } else { + pathDistinction = fmt.Sprintf(".vertices[%v]", jsonQuery.Relations[0].Depth.Max) + + } + } else { + pathDistinction = ".edges[**]" + } + + // Getting the attribute if there is one + if modifier.AttributeIndex != -1 { + if modifier.SelectedType == "entity" { + pathDistinction += fmt.Sprintf(".%v", jsonQuery.Entities[modifier.ID].Constraints[modifier.AttributeIndex].Attribute) + + } else { + pathDistinction += fmt.Sprintf(".%v", jsonQuery.Relations[modifier.ID].Constraints[modifier.AttributeIndex].Attribute) + + } + } + + // If count is used it has to be replaced with Length + unique else use the modifier type + if modifier.Type == "COUNT" { + ret += fmt.Sprintf("RETURN LENGTH (unique(r0[*]%v))", pathDistinction) + + } else { + ret += fmt.Sprintf("RETURN %v (r0[*]%v)", modifier.Type, pathDistinction) + + } + + } else { + // Check if the modifier is on an attribute + if modifier.AttributeIndex == -1 { + ret += fmt.Sprintf("RETURN LENGTH (n%v)", modifier.ID) + } else { + var attribute string + + // Selecting the right attribute from either the entity constraint or relation constraint + if modifier.SelectedType == "entity" { + attribute = jsonQuery.Entities[modifier.ID].Constraints[modifier.AttributeIndex].Attribute + + } else { + attribute = jsonQuery.Relations[modifier.ID].Constraints[modifier.AttributeIndex].Attribute + + } + + // If count is used it has to be replaced with Length + unique else use the modifier type + if modifier.Type == "COUNT" { + ret += fmt.Sprintf("RETURN LENGTH (unique(n%v[*].%v))", modifier.ID, attribute) + + } else { + ret += fmt.Sprintf("RETURN %v (n%v[*].%v)", modifier.Type, modifier.ID, attribute) + + } + } + } + + } else { + + // Create UNION statements that create unique lists of all the nodes and relations + // Thus removing all duplicates + nodeUnion = "\nLET nodes = first(RETURN UNION_DISTINCT(" + for _, relation := range relationsToReturn { + nodeUnion += fmt.Sprintf("flatten(%v[**].vertices), ", relation) + } + + for _, node := range nodesToReturn { + nodeUnion += fmt.Sprintf("%v,", node) + } + nodeUnion += "[],[]))\n" + + relationUnion = "LET edges = first(RETURN UNION_DISTINCT(" + for _, relation := range relationsToReturn { + relationUnion += fmt.Sprintf("flatten(%v[**].edges), ", relation) + } + relationUnion += "[],[]))\n" + + ret += nodeUnion + relationUnion + ret += "RETURN {\"vertices\":nodes, \"edges\":edges }" + + } + + return &ret +} + +/* createNodeLet generates a 'LET' statement for a node related query +Parameters: node is an entityStruct containing the information of a single node, +name is the autogenerated name of the node consisting of "n" + the index of the node + +Return: a string containing a single LET-statement in AQL +*/ +func createNodeLet(node *entity.QueryEntityStruct, name *string) *string { + header := fmt.Sprintf("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 +} + +/* createRelationLetWithFromEntity generates a 'LET' statement for relations with an 'EntityFrom' property and optionally an 'EntitiyTo' property +Parameters: relation is a relation struct containing the information of a single relation, +name is the autogenerated name of the node consisting of "r" + the index of the relation, +entities is a list of entityStructs that are needed to form the relation LET-statement + +Return: a string containing a single LET-statement in AQL +*/ +func createRelationLetWithFromEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int) *string { + header := fmt.Sprintf("LET %v = (\n\tFOR x IN n%v \n", name, relation.EntityFrom) + forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v OUTBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Type) + + // Guarantees that there is no path returned with a duplicate edge + // This way there are no cycle paths possible, TODO: more research about this needed + optionStmtn := "\tOPTIONS { uniqueEdges: \"path\" }\n" + + vFilterStmnt := "" + if relation.EntityTo != -1 { + // If there is a to-node, generate the filter statement + toConstraints := (*entities)[relation.EntityTo].Constraints + vFilterStmnt += *createConstraintStatements(&toConstraints, "v", false) + + // Add a WITH statement if the collection of entityTo is not yet included + if (*entities)[(*relation).EntityFrom].Type != (*entities)[(*relation).EntityTo].Type { + header = fmt.Sprintf("WITH %v\n %v", (*entities)[(*relation).EntityTo].Type, header) + } + } + + relationFilterStmnt := *createConstraintStatements(&relation.Constraints, "p", true) + + // Dont use a limit on quantifing queries + footer := "" + if limit != -1 { + footer += fmt.Sprintf("\tLIMIT %v \n", limit) + } + footer += "RETURN DISTINCT p )\n" + + ret := header + forStatement + optionStmtn + vFilterStmnt + relationFilterStmnt + footer + return &ret +} + +/* createRelationLetWithOnlyToEntity generates a 'LET' statement for relations with only an 'EntityTo' property +Parameters: relation is a relation struct containing the information of a single relation, +name is the autogenerated name of the node consisting of "r" + the index of the relation, +entities is a list of entityStructs that are needed to form the relation LET-statement + +Return: a string containing a single LET-statement in AQL +*/ +func createRelationLetWithOnlyToEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int) *string { + header := fmt.Sprintf("LET %v = (\n\tFOR x IN n%v \n", name, relation.EntityTo) + forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v INBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Type) + + // Guarantees that there is no path returned with a duplicate edge + // This way there are no cycle paths possible, TODO: more research about this needed + optionStmtn := "\tOPTIONS { uniqueEdges: \"path\" }\n" + + relationFilterStmnt := *createConstraintStatements(&relation.Constraints, "p", true) + + // Dont use a limit on quantifing queries + footer := "" + if limit != -1 { + footer += fmt.Sprintf("\tLIMIT %v \n", limit) + } + footer += "RETURN DISTINCT p )\n" + + ret := header + forStatement + optionStmtn + relationFilterStmnt + footer + return &ret +} diff --git a/convertQuery_test.go b/convertQuery_test.go new file mode 100644 index 0000000000000000000000000000000000000000..171c4ceb5b4ecd844a31e329505f25128c16d99e --- /dev/null +++ b/convertQuery_test.go @@ -0,0 +1,833 @@ +package aql + +import ( + "errors" + "strings" + "testing" + + "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 + }`) + + 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 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 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": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [], + "limit": 5000, + "modifiers": [ + { + "type": "COUNT", + "selectedType": "entity", + "id": 0, + "attributeIndex": -1 + } + ] + }`) + + 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)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": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [], + "limit": 5000, + "modifiers": [ + { + "type": "SUM", + "selectedType": "entity", + "id": 0, + "attributeIndex": 0 + } + ] + }`) + + 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)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": "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, + "modifiers": [ + { + "type": "COUNT", + "selectedType": "relation", + "id": 0, + "attributeIndex": -1 + } + ] + }`) + + 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 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": "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, + "modifiers": [ + { + "type": "AVG", + "selectedType": "relation", + "id": 0, + "attributeIndex": 0 + } + ] + }`) + + 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 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": "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) +} + +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": "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 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) +} + +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": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -4, + "entityTo": 0, + "constraints": [] + } + ], + "limit": 5000 + }`) + + _, _, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.NoError(t, err) +} diff --git a/createConstraints.go b/createConstraints.go new file mode 100644 index 0000000000000000000000000000000000000000..abe8035f0045fbf19acc5a65aacb067b9c731cd0 --- /dev/null +++ b/createConstraints.go @@ -0,0 +1,100 @@ +package aql + +import ( + "fmt" + + "git.science.uu.nl/datastrophe/aql-conversion/entity" +) + +/* createConstraintStatements generates the appropriate amount of constraint lines calling createConstraingBoolExpression +Parameters: constraints is a list of constraintStructs that specify the constraints of a node or relation, +name is the id of the corresponding relation/node, +isRelation is a boolean specifying if this constraint comes from a node or relation + +Return: a string containing a FILTER-statement with all the constraints +*/ +func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, name string, isRelation bool) *string { + s := "" + if len(*constraints) == 0 { + return &s + } + + newLineStatement := "\tFILTER" + + for _, v := range *constraints { + s += fmt.Sprintf("%v %v \n", newLineStatement, *createConstraintBoolExpression(&v, name, isRelation)) + newLineStatement = "\tAND" + } + + return &s +} + +/* createConstraintBoolExpression generates a single boolean expression, +e.g. {name}.city == "New York". + +Parameters: constraint is a single constraint of a node or relation, +name is the id of the corresponding relation/node, +isRelation is a boolean specifying if this constraint comes from a node or relation, that changes the structure of the expression + +Return: a string containing an boolean expression of a single constraint +*/ +func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, name string, isRelation bool) *string { + var ( + match string + value string + line string + ) + + // Constraint datatypes back end + // text MatchTypes: EQ/NEQ/contains/excludes + // number MatchTypes: EQ/NEQ/GT/LT/GET/LET + // bool MatchTypes: EQ/NEQ + + switch constraint.DataType { + case "text": + value = fmt.Sprintf("\"%s\"", constraint.Value) + switch constraint.MatchType { + case "NEQ": + match = "!=" + case "contains": + match = "LIKE" + value = fmt.Sprintf("\"%%%s%%\"", constraint.Value) + case "excludes": + match = "NOT LIKE" + value = fmt.Sprintf("\"%%%s%%\"", constraint.Value) + default: //EQ + match = "==" + } + case "number": + value = constraint.Value + switch constraint.MatchType { + case "NEQ": + match = "!=" + case "GT": + match = ">" + case "LT": + match = "<" + case "GET": + match = ">=" + case "LET": + match = "<=" + default: //EQ + match = "==" + } + default: /*bool*/ + value = constraint.Value + switch constraint.MatchType { + case "NEQ": + match = "!=" + default: //EQ + match = "==" + } + } + + if isRelation { + line = fmt.Sprintf("%s.edges[*].%s ALL %s %s", name, constraint.Attribute, match, value) + } else { + line = fmt.Sprintf("%s.%s %s %s", name, constraint.Attribute, match, value) + } + return &line +} diff --git a/entity/document.go b/entity/document.go new file mode 100644 index 0000000000000000000000000000000000000000..08ef36d21bf6d9943034e2080d49b8110e621266 --- /dev/null +++ b/entity/document.go @@ -0,0 +1,13 @@ +package entity + +// Document with Empty struct to retrieve all data from the DB Document +type Document map[string]interface{} + +// GeneralFormat with Empty struct to retrieve all data from the DB Document +type GeneralFormat map[string][]Document + +// ListContainer is a struct that keeps track of the nodes and edges that need to be returned +type ListContainer struct { + NodeList []Document + EdgeList []Document +} diff --git a/entity/queryStruct.go b/entity/queryStruct.go new file mode 100644 index 0000000000000000000000000000000000000000..14ecebe354f5bca3e54998917e697f9419ca9cf2 --- /dev/null +++ b/entity/queryStruct.go @@ -0,0 +1,60 @@ +package entity + +// QueryParsedJSON is used for JSON conversion of the incoming byte array +type QueryParsedJSON 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 + ID 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 +} + +// QueryConstraintStruct holds the information of the constraint +// Constraint datatypes +// text MatchTypes: exact/contains/startswith/endswith +// number MatchTypes: GT/LT/EQ +// bool MatchTypes: EQ/NEQ +type QueryConstraintStruct struct { + Attribute string + Value string + DataType string + MatchType string +} diff --git a/go.mod b/go.mod index 864120bcfb670a2664485a0af0280d2186ee2804..3a61254e0f4386dd9bb3bfb5c501a27bd42afac0 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.science.uu.nl/datastrophe/aql-conversion go 1.16 + +require github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..b380ae44575c322793da20d51d562f1d8aef1b1d --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/queryConverter.go b/queryConverter.go new file mode 100644 index 0000000000000000000000000000000000000000..9fa906bba7880cdb2273621aa7b8fd0fe9edbc2f --- /dev/null +++ b/queryConverter.go @@ -0,0 +1,9 @@ +package aql + +// Service implements the QueryConverter interface (in the query service) +type Service struct { +} + +func NewService() *Service { + return &Service{} +}