diff --git a/cypher/clustering.go b/cypher/clustering.go index 1504b1125a52c4039b98c5d95495bf011a9c0917..5a9a39519d9e191a41c65edd6a215bd33a39e5fc 100644 --- a/cypher/clustering.go +++ b/cypher/clustering.go @@ -1,6 +1,7 @@ package cypher import ( + "errors" "fmt" "git.science.uu.nl/graphpolaris/query-conversion/entity" @@ -175,3 +176,89 @@ func checkForQueryCluster(JSONQuery *entity.IncomingQueryJSON) (*entity.Incoming return &clusterJSON, &restJSON, isRest } + +// checkNoDeadEnds checks to see if al from's and to's exist +func checkNoDeadEnds(JSONQuery *entity.IncomingQueryJSON) (bool, error) { + + // Check for all the connections of a relation + for _, rel := range JSONQuery.Relations { + if rel.FromID != -1 { + if rel.FromType == "entity" { + ent := JSONQuery.FindE(rel.FromID) + if ent == nil { + return false, errors.New("Invalid query") + } + } else if rel.FromType == "groupBy" { + gb := JSONQuery.FindG(rel.FromID) + if gb == nil { + return false, errors.New("Invalid query") + } + } + } + + if rel.ToID != -1 { + if rel.ToType == "entity" { + ent := JSONQuery.FindE(rel.ToID) + if ent == nil { + return false, errors.New("Invalid query") + } + } else if rel.ToType == "groupBy" { + gb := JSONQuery.FindG(rel.ToID) + if gb == nil { + return false, errors.New("Invalid query") + } + } + } + } + + // Check for all the connections of a group by + for _, gb := range JSONQuery.GroupBys { + if gb.GroupType == "entity" { + ent := JSONQuery.FindE(gb.GroupID) + if ent == nil { + return false, errors.New("Invalid query") + } + } + + if gb.GroupType == "relation" { + rel := JSONQuery.FindE(gb.GroupID) + if rel == nil { + return false, errors.New("Invalid query") + } + } + + if gb.ByType == "entity" { + ent := JSONQuery.FindE(gb.ByID) + if ent == nil { + return false, errors.New("Invalid query") + } + } + + if gb.ByType == "relation" { + rel := JSONQuery.FindE(gb.ByID) + if rel == nil { + return false, errors.New("Invalid query") + } + } + } + + // Check all the connections of IN-statements + for _, ent := range JSONQuery.Entities { + if len(ent.Constraints) == 0 { + continue + } + + for _, cons := range ent.Constraints { + if cons.InID == -1 { + continue + } + + gb := JSONQuery.FindG(cons.InID) + if gb == nil { + return false, errors.New("Invalid query") + } + } + } + + return true, nil +} diff --git a/cypher/convertQuery.go b/cypher/convertQuery.go index 6575fe72e488e2bb1e25d9d65c6106d483f6ca44..e4d95375cd9e68c10bcf79dee0a0b3a1691dbf01 100644 --- a/cypher/convertQuery.go +++ b/cypher/convertQuery.go @@ -21,6 +21,11 @@ func (s *Service) ConvertQuery(totalJSONQuery *entity.IncomingQueryJSON) (*strin return nil, errors.New("Invalid query") } + ok, err := checkNoDeadEnds(query) + if !ok { + return nil, err + } + if isRest { fmt.Println("Rest:") fmt.Println(rest) @@ -28,7 +33,7 @@ func (s *Service) ConvertQuery(totalJSONQuery *entity.IncomingQueryJSON) (*strin // If something needs to be done with other query cluster, then add code here } - finalCypher, err := createCypher(query) + finalCypher, err = createCypher(query) if err != nil { return nil, err } @@ -45,6 +50,8 @@ func createCypher(JSONQuery *entity.IncomingQueryJSON) (*string, error) { return nil, err } + fmt.Println(hierarchy) + // translate it to cypher in the right order, using the hierarchy cypher, err := formQuery(JSONQuery, hierarchy) if err != nil { @@ -141,7 +148,6 @@ func createQueryHierarchy(JSONQuery *entity.IncomingQueryJSON) (entity.Query, er IDctr := 0 // Add relations all to query parts - log.Println(JSONQuery.Relations) for _, rel := range JSONQuery.Relations { part := entity.QueryPart{ @@ -156,35 +162,6 @@ func createQueryHierarchy(JSONQuery *entity.IncomingQueryJSON) (entity.Query, er } - // // Here comes a checker for (A)-->(B) and (B)-->(A). This can be converted to one relation with the other as a constraint - // // Lets call it a small cycle. It wont catch bigger cycles (with 3 nodes for example) - - // // ** Note: nog code schrijven die hiervan een goeie exist call kan maken - // // EN de return statement is ook belangrijk - // for i := 0; i < len(parts); i++ { - // rel := JSONQuery.FindR(parts[i].QID) - // if rel.FromID == -1 || rel.ToID == -1 { - // continue - // } - - // for j := i + 1; j < len(parts); j++ { - // other := JSONQuery.FindR(parts[j].QID) - - // if other.FromID == -1 || other.ToID == -1 { - // continue - // } - - // if (rel.ToID == other.FromID && rel.FromID == other.ToID) { - // // Thus a cycle: Add the part as a nested one and remove it from the query - - // parts[i].NestedPart = &parts[j] - // parts = entity.Remove(parts, j) - // j-- - - // } - // } - // } - // Add the Groupby's for _, gb := range JSONQuery.GroupBys { part := entity.QueryPart{ @@ -239,6 +216,7 @@ func createQueryHierarchy(JSONQuery *entity.IncomingQueryJSON) (entity.Query, er if rel.FromID == rela.ToID && rel.FromType == rela.ToType { part := parts.Find(rel.ID, "relation") part.Dependencies = append(part.Dependencies, parts.Find(rela.ID, "relation").PartID) + log.Println("yeet") } } @@ -297,21 +275,7 @@ func createQueryHierarchy(JSONQuery *entity.IncomingQueryJSON) (entity.Query, er } // Here comes a checker for (A)-->(B) and (B)-->(A). This is mitigated partly by ignoring it - // (It appears the wrong is with neo4j in returning strange results, after querying the same query in 4 different ways) // Lets call it a small cycle. It wont catch bigger cycles (with 3 nodes for example) - // It needs to create a new type of constraint (which createConstraints.go needs to be able to handle) - - // ** NOTE: dit is nogal even een open kwestie, want cypher geeft vage antwoorden op simpele queries met dit principe, - // er komen vage extra relations bij die niet aan de patronen voldoen, terwijl je zou zeggen dat het simpel moet zijn - // en cypher heeft geen INTERSECT maar wel een UNION #hoedan - - // Misschien werkt dit al, want dit werkt ook: - // Match (a:Person)-[r:ACTED_IN]-(m:Movie) - // with * - // Match (m:Movie)-[f:DIRECTED]-(a) - // return count (*) - - // ** Kijk of dit ook nog werkt als er andere relations afhangen van deze cycle!! for _, p := range parts { // We only allow small cycles with relations @@ -326,19 +290,23 @@ func createQueryHierarchy(JSONQuery *entity.IncomingQueryJSON) (entity.Query, er continue } + // Deleting from a slice while looping through it is an easy way to make mistakes, hence the workaround cycle := false toRemove := -1 for i, otherDep := range other.Dependencies { if otherDep == p.PartID { - // small cycle detected + // Small cycle detected cycle = true toRemove = i } } + // Remove one of the two dependencies, does not really matter which, cypher knits it back together due to the query + // using the same ID's, thus making it a cycle again later on. if cycle { + log.Println("Cycle detected and removed") if len(other.Dependencies) == 0 { other.Dependencies = make([]int, 0) } else { @@ -492,8 +460,6 @@ func createInCypher(JSONQuery *entity.IncomingQueryJSON, part entity.QueryPart) retStatement := match + eConstraints + with return &retStatement, nil - // Should be able to handle multiple IN statements from one entity, not sure if that will ever happen - // TODO: test this } // createRelationCypher takes the json and a query part, finds the necessary entities and converts it into cypher diff --git a/cypher/convertQuery_test.go b/cypher/convertQuery_test.go index 636353e6f771842e9ad13dcaae213e602943cca0..01de62f85542165dca4cb36e07a39fc27999c787 100644 --- a/cypher/convertQuery_test.go +++ b/cypher/convertQuery_test.go @@ -2,6 +2,7 @@ package cypher import ( "encoding/json" + "errors" "fmt" "strings" "testing" @@ -12,8 +13,7 @@ import ( // All these tests test the entire flow -func Test1(t *testing.T) { - // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D +func TestGroupBy(t *testing.T) { query := []byte(`{ "databaseName": "Movies3", "return": { @@ -121,9 +121,8 @@ func Test1(t *testing.T) { UNWIND relationships(p1) as r1 WITH * RETURN r1, eg1, e2 - LIMIT 5000` + LIMIT 5000;nodelink` - fmt.Println(*cypher) trimmedCypher := strings.Replace(*cypher, "\n", "", -1) trimmedCypher = strings.Replace(trimmedCypher, "\t", "", -1) @@ -131,11 +130,10 @@ func Test1(t *testing.T) { trimmedAnswer = strings.Replace(trimmedAnswer, "\t", "", -1) fmt.Println(*cypher) - assert.Equal(t, trimmedAnswer, trimmedAnswer) + assert.Equal(t, trimmedAnswer, trimmedCypher) } -func Test2(t *testing.T) { - // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D +func TestSmallChain(t *testing.T) { query := []byte(`{ "databaseName": "TweedeKamer", "return": { @@ -227,7 +225,7 @@ func Test2(t *testing.T) { UNWIND relationships(p1) as r1 WITH * RETURN r1, e0, e2, r0, e0, e1 - LIMIT 5000` + LIMIT 5000;nodelink` fmt.Println(*cypher) @@ -238,11 +236,12 @@ func Test2(t *testing.T) { trimmedAnswer = strings.Replace(trimmedAnswer, "\t", "", -1) fmt.Println(*cypher) - assert.Equal(t, trimmedAnswer, trimmedAnswer) + assert.Equal(t, trimmedAnswer, trimmedCypher) } -func Test3(t *testing.T) { - // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D + +// This one does not really have dependencies, the order doesnt matter, maybe sort on numbers between equal dependencies? +func TestLargeQueryChain(t *testing.T) { query := []byte(`{ "databaseName": "TweedeKamer", "return": { @@ -345,7 +344,7 @@ func Test3(t *testing.T) { "max": 1 }, "fromType": "entity", - "fromID": 0, + "fromID": 1, "toType": "entity", "toID": 2, "constraints": [] @@ -358,9 +357,9 @@ func Test3(t *testing.T) { "max": 1 }, "fromType": "entity", - "fromID": 3, + "fromID": 2, "toType": "entity", - "toID": 2, + "toID": 3, "constraints": [] }, { @@ -398,12 +397,12 @@ func Test3(t *testing.T) { AND e1.seats < 10 UNWIND relationships(p0) as r0 WITH * - MATCH p1 = (e0:parliament)-[:submits*1..1]-(e2:resolutions) - WHERE e0.name CONTAINS "%A%" + MATCH p1 = (e1:parties)-[:submits*1..1]-(e2:resolutions) + WHERE e1.seats < 10 AND e2.date CONTAINS "%mei%" UNWIND relationships(p1) as r1 WITH * - MATCH p2 = (e3:parliament)-[:submits*1..1]-(e2:resolutions) + MATCH p2 = (e2:resolutions)-[:submits*1..1]-(e3:parliament) WHERE e2.date CONTAINS "%mei%" UNWIND relationships(p2) as r2 WITH * @@ -411,10 +410,8 @@ func Test3(t *testing.T) { WHERE e4.name = "Volkspartij voor Vrijheid en Democratie" UNWIND relationships(p3) as r3 WITH * - RETURN r3, e3, e4, r2, e3, e2, r1, e0, e2, r0, e0, e1 - LIMIT 5000` - - fmt.Println(*cypher) + RETURN r3, e3, e4, r2, e2, e3, r1, e1, e2, r0, e0, e1 + LIMIT 5000;nodelink` trimmedCypher := strings.Replace(*cypher, "\n", "", -1) trimmedCypher = strings.Replace(trimmedCypher, "\t", "", -1) @@ -423,177 +420,75 @@ func Test3(t *testing.T) { trimmedAnswer = strings.Replace(trimmedAnswer, "\t", "", -1) fmt.Println(*cypher) - assert.Equal(t, trimmedAnswer, trimmedAnswer) + assert.Equal(t, trimmedAnswer, trimmedCypher) } -func Test4(t *testing.T) { - // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D +func TestInStatement(t *testing.T) { query := []byte(`{ + "databaseName": "Movies3", "entities": [ { - "name": "parliament", - "ID": 0, - "constraints": [ - { - "attribute": "name", - "value": "Geert", - "dataType": "string", - "matchType": "contains", - "inID": -1, - "inType": "" - } - ] - }, - { - "name": "commissions", - "ID": 1, + "id": 0, + "name": "Person", "constraints": [] }, { - "name": "parliament", - "ID": 2, + "id": 1, + "name": "Movie", "constraints": [] }, { - "name": "parties", - "ID": 3, - "constraints": [ - { - "attribute": "seats", - "value": "10", - "dataType": "int", - "matchType": "LT", - "inID": -1, - "inType": "" - } - ] - }, - { - "name": "resolutions", - "ID": 4, + "id": 2, + "name": "Person", "constraints": [ { - "attribute": "date", - "value": "mei", + "attribute": "bornIn", + "value": "", "dataType": "string", - "matchType": "contains", - "inID": -1, - "inType": "" + "matchType": "", + "inID": 0, + "inType": "groupBy" } ] - }, - { - "name": "resolutions", - "ID": 5, - "constraints": [] - }, - { - "name": "parties", - "ID": 6, - "constraints": [] - } - , - { - "name": "parliament", - "ID": 7, - "constraints": [] } - ], - "groupBys": [], "relations": [ { - "ID": 0, - "name": "part_of", - "depth": { - "min": 1, - "max": 1 - }, - "fromType": "entity", - "fromId": 0, - "toType": "entity", - "toID": 1, - "constraints": [] - }, - { - "ID": 1, - "name": "part_of", + "id": 0, + "name": "ACTED_IN", "depth": { "min": 1, "max": 1 }, "fromType": "entity", - "fromId": 2, + "fromID": 0, "toType": "entity", "toID": 1, "constraints": [] - }, - { - "ID": 2, - "name": "member_of", - "depth": { - "min": 1, - "max": 1 - }, - "fromType": "entity", - "fromId": 2, - "toType": "entity", - "toID": 3, - "constraints": [] - }, - { - "ID": 3, - "name": "submits", - "depth": { - "min": 1, - "max": 1 - }, - "fromType": "entity", - "fromId": 2, - "toType": "entity", - "toID": 4, - "constraints": [] - }, - { - "ID": 4, - "name": "submits", - "depth": { - "min": 1, - "max": 1 - }, - "fromType": "entity", - "fromId": 0, - "toType": "entity", - "toID": 5, - "constraints": [] - }, - { - "ID": 5, - "name": "member_of", - "depth": { - "min": 1, - "max": 1 - }, - "fromType": "entity", - "fromId": 0, - "toType": "entity", - "toID": 6, - "constraints": [] } - , + ], + "groupBys": [ { - "ID": 6, - "name": "member_of", - "depth": { - "min": 1, - "max": 1 - }, - "fromType": "entity", - "fromId": 7, - "toType": "entity", - "toID": 6, - "constraints": [] + "id": 0, + "groupType": "entity", + "groupID": 1, + "groupAttribute": "imdbRating", + "byType": "entity", + "byID": 0, + "byAttribute": "bornIn", + "appliedModifier": "AVG", + "relationID": 0, + "constraints": [ + { + "attribute": "imdbRating", + "value": "7.5", + "dataType": "int", + "matchType": "GT", + "inID": -1, + "inType": "" + } + ] } ], "machineLearning": [], @@ -601,6 +496,17 @@ func Test4(t *testing.T) { } `) + answer := `MATCH p0 = (e0:Person)-[:ACTED_IN*1..1]-(e1:Movie) + UNWIND relationships(p0) as r0 + WITH * + WITH e0.bornIn AS e0_bornIn, AVG(e1.imdbRating) AS AVG_imdbRating + WHERE AVG_imdbRating > 7.5 + MATCH (e2:Person) + WHERE e2.bornIn IN e0_bornIn + WITH * + RETURN e2 + LIMIT 5000;nodelink` + var JSONQuery entity.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) @@ -610,36 +516,6 @@ func Test4(t *testing.T) { fmt.Println(err) } - answer := `MATCH p2 = (e2:parliament)-[:member_of*1..1]-(e3:parties) - WHERE e3.seats < 10 - UNWIND relationships(p2) as r2 - WITH * - MATCH p3 = (e2:parliament)-[:submits*1..1]-(e4:resolutions) - WHERE e4.date CONTAINS "%mei%" - UNWIND relationships(p3) as r3 - WITH * - MATCH p6 = (e7:parliament)-[:member_of*1..1]-(e6:parties) - UNWIND relationships(p6) as r6 - WITH * - MATCH p4 = (e0:parliament)-[:submits*1..1]-(e5:resolutions) - WHERE e0.name CONTAINS "%%Geert%" - UNWIND relationships(p4) as r4 - WITH * - MATCH p0 = (e0:parliament)-[:part_of*1..1]-(e1:commissions) - WHERE e0.name CONTAINS "%%Geert%" - UNWIND relationships(p0) as r0 - WITH * - MATCH p5 = (e0:parliament)-[:member_of*1..1]-(e6:parties) - WHERE e0.name CONTAINS "%%Geert%" - UNWIND relationships(p5) as r5 - WITH * - MATCH p1 = (e2:parliament)-[:part_of*1..1]-(e1:commissions) - UNWIND relationships(p1) as r1 - WITH * - RETURN r1, e2, e1, r5, e0, e6, r0, e0, e1, r4, e0, e5, r6, e7, e6, r3, e2, e4, r2, e2, e3 - LIMIT 5000` - - fmt.Println(*cypher) trimmedCypher := strings.Replace(*cypher, "\n", "", -1) trimmedCypher = strings.Replace(trimmedCypher, "\t", "", -1) @@ -647,11 +523,10 @@ func Test4(t *testing.T) { trimmedAnswer = strings.Replace(trimmedAnswer, "\t", "", -1) fmt.Println(*cypher) - assert.Equal(t, trimmedAnswer, trimmedAnswer) + assert.Equal(t, trimmedAnswer, trimmedCypher) } -func Test5(t *testing.T) { - // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D +func TestDoubleInStatement(t *testing.T) { query := []byte(`{ "databaseName": "Movies3", "entities": [ @@ -678,6 +553,20 @@ func Test5(t *testing.T) { "inType": "groupBy" } ] + }, + { + "id": 3, + "name": "Person", + "constraints": [ + { + "attribute": "bornIn", + "value": "", + "dataType": "string", + "matchType": "", + "inID": 0, + "inType": "groupBy" + } + ] } ], "relations": [ @@ -731,8 +620,11 @@ func Test5(t *testing.T) { MATCH (e2:Person) WHERE e2.bornIn IN e0_bornIn WITH * - RETURN e2 - LIMIT 5000` + MATCH (e3:Person) + WHERE e3.bornIn IN e0_bornIn + WITH * + RETURN e3 + LIMIT 5000;nodelink` var JSONQuery entity.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) @@ -750,11 +642,10 @@ func Test5(t *testing.T) { trimmedAnswer = strings.Replace(trimmedAnswer, "\t", "", -1) fmt.Println(*cypher) - assert.Equal(t, trimmedAnswer, trimmedAnswer) + assert.Equal(t, trimmedAnswer, trimmedCypher) } -func Test6(t *testing.T) { - // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D +func TestSimpleQuery(t *testing.T) { query := []byte(`{ "return": { "entities": [ @@ -800,10 +691,14 @@ func Test6(t *testing.T) { } `) + answer := `MATCH p0 = (e11:Person)-[:DIRECTED*1..1]-(e12:Movie) + UNWIND relationships(p0) as r10 + WITH * + RETURN r10, e11, e12 + LIMIT 5000;nodelink` + var JSONQuery entity.IncomingQueryJSON json.Unmarshal(query, &JSONQuery) - fmt.Println(JSONQuery) - fmt.Println(" ") s := NewService() cypher, err := s.ConvertQuery(&JSONQuery) @@ -811,11 +706,16 @@ func Test6(t *testing.T) { fmt.Println(err) } + trimmedCypher := strings.Replace(*cypher, "\n", "", -1) + trimmedCypher = strings.Replace(trimmedCypher, "\t", "", -1) + + trimmedAnswer := strings.Replace(answer, "\n", "", -1) + trimmedAnswer = strings.Replace(trimmedAnswer, "\t", "", -1) + fmt.Println(*cypher) - t.Fail() + assert.Equal(t, trimmedAnswer, trimmedCypher) } func TestNoRelation(t *testing.T) { - // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D query := []byte(`{ "return": { "entities": [ @@ -863,7 +763,6 @@ func TestNoRelation(t *testing.T) { } func TestNoEntities(t *testing.T) { - // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D query := []byte(`{ "return": { "entities": [ @@ -912,8 +811,7 @@ func TestNoEntities(t *testing.T) { t.Fail() } } -func TestTwoRelations(t *testing.T) { - // Works, but, the AVG function is applied to a string, so that doesnt work, but the translation does :D +func TestTwoRelationsCycle(t *testing.T) { query := []byte(`{ "return": { "entities": [ @@ -989,25 +887,247 @@ func TestTwoRelations(t *testing.T) { t.Fail() return } + + answer1 := `MATCH p0 = (e11:Person)-[:DIRECTED*1..1]-(e12:Movie) + UNWIND relationships(p0) as r10 + WITH * + MATCH p1 = (e11:Person)-[:DIRECTED*1..1]-(e12:Movie) + UNWIND relationships(p1) as r10 + WITH * + RETURN r10, e11, e12, r10, e11, e12 + LIMIT 5000;nodelink` + + answer2 := `MATCH p1 = (e12:Movie)-[:ACTED_IN*1..1]-(e11:Person) + UNWIND relationships(p1) as r11 + WITH * + MATCH p0 = (e11:Person)-[:DIRECTED*1..1]-(e12:Movie) + UNWIND relationships(p0) as r10 + WITH * + RETURN r10, e11, e12, r11, e12, e11 + LIMIT 5000;nodelink` + + trimmedCypher := strings.Replace(*cypher, "\n", "", -1) + trimmedCypher = strings.Replace(trimmedCypher, "\t", "", -1) + + trimmedAnswer1 := strings.Replace(answer1, "\n", "", -1) + trimmedAnswer1 = strings.Replace(trimmedAnswer1, "\t", "", -1) + trimmedAnswer2 := strings.Replace(answer2, "\n", "", -1) + trimmedAnswer2 = strings.Replace(trimmedAnswer2, "\t", "", -1) + fmt.Println(*cypher) - t.Fail() - // fmt.Println(*cypher) + // Both answers are correct + if !(trimmedAnswer1 == trimmedCypher || trimmedAnswer2 == trimmedCypher) { + t.Fail() + } +} + +func TestCyclePlusDependency(t *testing.T) { + query := []byte(`{ + "return": { + "entities": [ + 11, + 12 + ], + "relations": [ + 10 + ], + "groupBys": [] + }, + "entities": [ + { + "name": "Person", + "ID": 11, + "constraints": [] + }, + { + "name": "Movie", + "ID": 12, + "constraints": [] + }, + { + "name": "Person", + "ID": 13, + "constraints": [] + } + ], + "relations": [ + { + "ID": 10, + "name": "DIRECTED", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 11, + "toType": "entity", + "toID": 12, + "constraints": [] + }, + { + "ID": 11, + "name": "ACTED_IN", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 12, + "toType": "entity", + "toID": 11, + "constraints": [] + }, + { + "ID": 12, + "name": "ACTED_IN", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 13, + "toType": "entity", + "toID": 12, + "constraints": [] + } + ], + "groupBys": [], + "machineLearning": [], + "limit": 5000, + "databaseName": "Movies3" + } + `) + + var JSONQuery entity.IncomingQueryJSON + json.Unmarshal(query, &JSONQuery) + fmt.Println(JSONQuery) + fmt.Println(" ") + + s := NewService() + cypher, err := s.ConvertQuery(&JSONQuery) + if err != nil { + fmt.Println(err) + t.Fail() + return + } - // answer := `MATCH p0 = (e11:Person)-[:DIRECTED*1..1]-(e12:Movie) - // UNWIND relationships(p0) as r10 - // WITH * - // MATCH p1 = (e11:Person)-[:DIRECTED*1..1]-(e12:Movie) - // UNWIND relationships(p1) as r10 - // WITH * - // RETURN r10, e11, e12, r10, e11, e12 - // LIMIT 5000;nodelink` + if cypher == nil { + t.Fail() + return + } - // trimmedCypher := strings.Replace(*cypher, "\n", "", -1) - // trimmedCypher = strings.Replace(trimmedCypher, "\t", "", -1) + answer := `MATCH p2 = (e13:Person)-[:ACTED_IN*1..1]-(e12:Movie) + UNWIND relationships(p2) as r12 + WITH * + MATCH p1 = (e12:Movie)-[:ACTED_IN*1..1]-(e11:Person) + UNWIND relationships(p1) as r11 + WITH * + MATCH p0 = (e11:Person)-[:DIRECTED*1..1]-(e12:Movie) + UNWIND relationships(p0) as r10 + WITH * + RETURN r10, e11, e12, r11, e12, e11, r12, e13, e12 + LIMIT 5000;nodelink` + + trimmedCypher := strings.Replace(*cypher, "\n", "", -1) + trimmedCypher = strings.Replace(trimmedCypher, "\t", "", -1) - // trimmedAnswer := strings.Replace(answer, "\n", "", -1) - // trimmedAnswer = strings.Replace(trimmedAnswer, "\t", "", -1) + trimmedAnswer := strings.Replace(answer, "\n", "", -1) + trimmedAnswer = strings.Replace(trimmedAnswer, "\t", "", -1) - // fmt.Println(*cypher) - // assert.Equal(t, trimmedAnswer, trimmedAnswer) + fmt.Println(*cypher) + assert.Equal(t, trimmedAnswer, trimmedCypher) +} +func TestTripleCycle(t *testing.T) { + query := []byte(`{ + "return": { + "entities": [ + 11, + 12 + ], + "relations": [ + 10 + ], + "groupBys": [] + }, + "entities": [ + { + "name": "Person", + "ID": 11, + "constraints": [] + }, + { + "name": "Movie", + "ID": 12, + "constraints": [] + }, + { + "name": "Person", + "ID": 13, + "constraints": [] + } + ], + "relations": [ + { + "ID": 10, + "name": "DIRECTED", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 11, + "toType": "entity", + "toID": 12, + "constraints": [] + }, + { + "ID": 11, + "name": "ACTED_IN", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 12, + "toType": "entity", + "toID": 13, + "constraints": [] + }, + { + "ID": 12, + "name": "ACTED_IN", + "depth": { + "min": 1, + "max": 1 + }, + "fromType": "entity", + "fromID": 13, + "toType": "entity", + "toID": 11, + "constraints": [] + } + ], + "groupBys": [], + "machineLearning": [], + "limit": 5000, + "databaseName": "Movies3" + } + `) + + var JSONQuery entity.IncomingQueryJSON + json.Unmarshal(query, &JSONQuery) + + s := NewService() + cypher, err := s.ConvertQuery(&JSONQuery) + if err != nil { + fmt.Println(err) + assert.Equal(t, err, errors.New("Cyclic query detected")) + return + } + + if cypher == nil { + t.Fail() + return + } + t.Fail() } diff --git a/entity/queryStruct.go b/entity/queryStruct.go index fe43b58524e8dbf14281ff157efd9c73bb20926a..ea069ee5e3d53c2e4b056ebca724dbf607a49933 100644 --- a/entity/queryStruct.go +++ b/entity/queryStruct.go @@ -40,6 +40,7 @@ type QueryRelationStruct struct { Constraints []QueryConstraintStruct `json:"constraints"` } +// QueryGroupByStruct holds all the info needed to form a group by type QueryGroupByStruct struct { ID int `json:"id"` GroupType string `json:"groupType"` @@ -67,6 +68,7 @@ type QueryConstraintStruct struct { InType string `json:"inType"` } +// QueryMLStruct holds info for machinelearning type QueryMLStruct struct { Queuename string Parameters []string @@ -116,6 +118,7 @@ func (JSONQuery IncomingQueryJSON) FindG(qID int) *QueryGroupByStruct { return nil } +// QueryPart is a struct containing a part of the query and a list of dependencies on which this part of the query depends type QueryPart struct { QType string // Eg if it is a relation or groupby QID int // ID of said relation/gb @@ -124,8 +127,10 @@ type QueryPart struct { NestedPart *QueryPart // Pointer to another part, used in some cases to avoid cycles } +// Query is a list of (possibly unordered) queryparts type Query []QueryPart +// Find retrieves a QueryPart based on the query's specifications func (q Query) Find(qID int, qType string) *QueryPart { for i := range q { if q[i].QID == qID && q[i].QType == qType { @@ -135,6 +140,7 @@ func (q Query) Find(qID int, qType string) *QueryPart { return nil } +// SelectByID retrieves a QueryPart based on its PartID func (q Query) SelectByID(ID int) *QueryPart { for i := range q { if q[i].PartID == ID {