From 1261fecfb63a7069fdb28a519141d4a026fa9d12 Mon Sep 17 00:00:00 2001 From: "Geurtjens,D. (Douwe Geurtjens)" <d.geurtjens@students.uu.nl> Date: Tue, 1 Feb 2022 08:09:25 +0000 Subject: [PATCH] Develop --- entity/IncomingTranslation.go | 17 ++ entity/ReturnFormat.go | 29 +++ main/main.go | 75 ++++--- main/sparql-mock-result.json | 103 +++++++++ {arangodb => usecases/arangodb}/cover.html | 0 .../arangodb}/data/.gitignore | 0 .../arangodb}/entity/document.go | 3 +- .../arangodb}/executeQuery.go | 41 ++-- .../arangodb}/executeQuery_test.go | 53 +++++ .../arangodb}/queryExecutor.go | 0 {arangodb => usecases/arangodb}/test.sh | 0 {neo4j => usecases/neo4j}/entity/types.go | 0 {neo4j => usecases/neo4j}/executeQuery.go | 2 +- .../neo4j}/executeQuery_test.go | 0 {neo4j => usecases/neo4j}/queryExecutor.go | 0 usecases/sparql/entity/result.go | 53 +++++ usecases/sparql/executeQuery.go | 197 ++++++++++++++++++ usecases/sparql/executeQuery_test.go | 46 ++++ usecases/sparql/interface.go | 21 ++ 19 files changed, 588 insertions(+), 52 deletions(-) create mode 100644 entity/IncomingTranslation.go create mode 100644 entity/ReturnFormat.go create mode 100644 main/sparql-mock-result.json rename {arangodb => usecases/arangodb}/cover.html (100%) rename {arangodb => usecases/arangodb}/data/.gitignore (100%) rename {arangodb => usecases/arangodb}/entity/document.go (93%) rename {arangodb => usecases/arangodb}/executeQuery.go (74%) rename {arangodb => usecases/arangodb}/executeQuery_test.go (83%) rename {arangodb => usecases/arangodb}/queryExecutor.go (100%) rename {arangodb => usecases/arangodb}/test.sh (100%) mode change 100755 => 100644 rename {neo4j => usecases/neo4j}/entity/types.go (100%) rename {neo4j => usecases/neo4j}/executeQuery.go (98%) rename {neo4j => usecases/neo4j}/executeQuery_test.go (100%) rename {neo4j => usecases/neo4j}/queryExecutor.go (100%) create mode 100644 usecases/sparql/entity/result.go create mode 100644 usecases/sparql/executeQuery.go create mode 100644 usecases/sparql/executeQuery_test.go create mode 100644 usecases/sparql/interface.go diff --git a/entity/IncomingTranslation.go b/entity/IncomingTranslation.go new file mode 100644 index 0000000..a45a1b7 --- /dev/null +++ b/entity/IncomingTranslation.go @@ -0,0 +1,17 @@ +/* +This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course. +© Copyright Utrecht University (Department of Information and Computing Sciences) +*/ + +package entity + +/* +IncomingTranslation contains the translated query and some possible metadata, + the metadata should have a clearly defined type for each usecase. + Each usecase should be responsible for unmarshalling to their own type + This really is a usecase for generics, but Go doesn't have them yet. +*/ +type IncomingTranslation struct { + Translation string + MetaData interface{} +} diff --git a/entity/ReturnFormat.go b/entity/ReturnFormat.go new file mode 100644 index 0000000..0960c08 --- /dev/null +++ b/entity/ReturnFormat.go @@ -0,0 +1,29 @@ +/* +This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course. +© Copyright Utrecht University (Department of Information and Computing Sciences) +*/ + +package entity + +/* +Node is a JSON format for nodes +*/ +type Node struct { + ID string `json:"id"` + Attributes map[string]interface{} `json:"attributes"` +} + +/* +Edge is a JSON format for edges +*/ +type Edge struct { + ID string `json:"id"` + From string `json:"from"` + To string `json:"to"` + Attributes map[string]interface{} `json:"attributes"` +} + +/* +ResultFormat is a format for the result, the interface{} can be either an edge or a node +*/ +type ResultFormat map[string][]interface{} diff --git a/main/main.go b/main/main.go index e183f8a..e68c6f0 100644 --- a/main/main.go +++ b/main/main.go @@ -6,48 +6,55 @@ This program has been developed by students from the bachelor Computer Science a package main import ( + "fmt" "log" - "git.science.uu.nl/graphpolaris/query-execution/arangodb" + "git.science.uu.nl/graphpolaris/query-execution/usecases/arangodb" + "git.science.uu.nl/graphpolaris/query-execution/usecases/sparql" ) /* main is used to test the different queryExecutors */ -func main() { +func main2() { queryservice := arangodb.NewService() - js := `WITH bank - LET n0 = ( - FOR x IN person - RETURN x + js := `LET result = ( + FOR e_29 IN parliament + LET e30 = ( + FOR e_30 IN parties + FOR r28 IN member_of + FILTER r28._from == e_29._id AND r28._to == e_30._id + FILTER length(e_30) != 0 AND length(r28) != 0 + RETURN {"nodes":union_distinct([e_30], []), "rel": union_distinct([r28], []), "mod": union_distinct([], [])} + ) + FILTER length(e30) != 0 AND length(e_29) != 0 + RETURN {"nodes":union_distinct(flatten(e30[**].nodes), [e_29]), "rel": union_distinct(flatten(e30[**].rel), []), "mod": union_distinct(flatten(e30[**].mod), [])} ) - LET r0 = ( - FOR x IN n0 - FOR v, e, p IN 1..1 OUTBOUND x transfers - 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 }` - - // tweedeKamer := `WITH parties - // LET n0 = ( - // FOR x IN parliament - // RETURN x - // ) - // LET r0 = ( - // FOR x IN n0 - // FOR v, e, p IN 1..1 OUTBOUND x member_of - // 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 }` + LET nodes = union_distinct(flatten(result[**].nodes),[]) + LET edges = union_distinct(flatten(result[**].rel),[]) + RETURN {"vertices":nodes,"edges":edges}` - result, err := queryservice.ExecuteQuery(js, "root", "DikkeDraak", "http://167.71.8.197", 8529, "UsecaseFraud") + result, err := queryservice.ExecuteQuery(js, "root", "DikkeDraak", "https://datastrophe.science.uu.nl/", 8529, "TweedeKamer") log.Println(err) - log.Println(result) + log.Println(string(*result)) +} + +func main() { + queryservice := sparql.NewService() + trips := `{ + "triples": { + "r0": { + "from": "e0", + "to": "e1" + }, + "r1": { + "from": "e2", + "to": "e1" + } + } + }` + res, err := queryservice.ExecuteQuery("lol", trips, "sparql-mock-result.json") + if err != nil { + fmt.Println(err) + } + fmt.Println(string(*res)) } diff --git a/main/sparql-mock-result.json b/main/sparql-mock-result.json new file mode 100644 index 0000000..f95007e --- /dev/null +++ b/main/sparql-mock-result.json @@ -0,0 +1,103 @@ +{ + "head": { + "vars": [ + "e0", + "e1", + "r0", + "e2", + "r1" + ] + }, + "results": { + "bindings": [ + { + "e0": { + "type": "uri", + "value": "http://api.stardog.com/Sierra_Swan" + }, + "e1": { + "type": "uri", + "value": "http://api.stardog.com/The_Black_Eyed_Peas" + }, + "r0": { + "type": "uri", + "value": "http://api.stardog.com/memberOf" + }, + "e2": { + "type": "uri", + "value": "http://api.stardog.com/Apl.de.ap" + }, + "r1": { + "type": "uri", + "value": "http://api.stardog.com/memberOf" + } + }, + { + "e0": { + "type": "uri", + "value": "http://api.stardog.com/Sierra_Swan" + }, + "e1": { + "type": "uri", + "value": "http://api.stardog.com/The_Black_Eyed_Peas" + }, + "r0": { + "type": "uri", + "value": "http://api.stardog.com/memberOf" + }, + "e2": { + "type": "uri", + "value": "http://api.stardog.com/Fergie_(singer)" + }, + "r1": { + "type": "uri", + "value": "http://api.stardog.com/memberOf" + } + }, + { + "e0": { + "type": "uri", + "value": "http://api.stardog.com/Sierra_Swan" + }, + "e1": { + "type": "uri", + "value": "http://api.stardog.com/The_Black_Eyed_Peas" + }, + "r0": { + "type": "uri", + "value": "http://api.stardog.com/memberOf" + }, + "e2": { + "type": "uri", + "value": "http://api.stardog.com/Sierra_Swan" + }, + "r1": { + "type": "uri", + "value": "http://api.stardog.com/memberOf" + } + }, + { + "e0": { + "type": "uri", + "value": "http://api.stardog.com/Sierra_Swan" + }, + "e1": { + "type": "uri", + "value": "http://api.stardog.com/The_Black_Eyed_Peas" + }, + "r0": { + "type": "uri", + "value": "http://api.stardog.com/memberOf" + }, + "e2": { + "type": "uri", + "value": "http://api.stardog.com/Taboo_(rapper)" + }, + "r1": { + "type": "uri", + "value": "http://api.stardog.com/memberOf" + } + } + ] + } +} \ No newline at end of file diff --git a/arangodb/cover.html b/usecases/arangodb/cover.html similarity index 100% rename from arangodb/cover.html rename to usecases/arangodb/cover.html diff --git a/arangodb/data/.gitignore b/usecases/arangodb/data/.gitignore similarity index 100% rename from arangodb/data/.gitignore rename to usecases/arangodb/data/.gitignore diff --git a/arangodb/entity/document.go b/usecases/arangodb/entity/document.go similarity index 93% rename from arangodb/entity/document.go rename to usecases/arangodb/entity/document.go index b6106cb..cdb30af 100644 --- a/arangodb/entity/document.go +++ b/usecases/arangodb/entity/document.go @@ -3,7 +3,7 @@ This program has been developed by students from the bachelor Computer Science a © Copyright Utrecht University (Department of Information and Computing Sciences) */ -package entity +package entityarangodb /* Document with Empty struct to retrieve all data from the DB Document @@ -21,4 +21,5 @@ ListContainer is a struct that keeps track of the nodes and edges that need to b type ListContainer struct { NodeList []Document EdgeList []Document + RowList []Document } diff --git a/arangodb/executeQuery.go b/usecases/arangodb/executeQuery.go similarity index 74% rename from arangodb/executeQuery.go rename to usecases/arangodb/executeQuery.go index 72bb845..d2a9959 100644 --- a/arangodb/executeQuery.go +++ b/usecases/arangodb/executeQuery.go @@ -11,7 +11,7 @@ import ( "encoding/json" "fmt" - "git.science.uu.nl/graphpolaris/query-execution/arangodb/entity" + entityarangodb "git.science.uu.nl/graphpolaris/query-execution/usecases/arangodb/entity" driver "github.com/arangodb/go-driver" "github.com/arangodb/go-driver/http" ) @@ -29,7 +29,7 @@ ExecuteQuery executes the given query on an ArangoDB instance func (s *Service) ExecuteQuery(query string, databaseUsername string, databasePassword string, databaseHost string, databasePort int, internalDatabase string) (*[]byte, error) { // Create data structure to store query result in - var queryResult = make(map[string][]entity.Document) + var queryResult = make(map[string][]entityarangodb.Document) // Open connection to ArangoDB conn, err := http.NewConnection(http.ConnectionConfig{ @@ -62,9 +62,10 @@ func (s *Service) ExecuteQuery(query string, databaseUsername string, databasePa defer cursor.Close() // Loop through the resulting documents - listContainer := entity.ListContainer{} - listContainer.EdgeList = make([]entity.Document, 0) - listContainer.NodeList = make([]entity.Document, 0) + listContainer := entityarangodb.ListContainer{} + listContainer.EdgeList = make([]entityarangodb.Document, 0) + listContainer.NodeList = make([]entityarangodb.Document, 0) + listContainer.RowList = make([]entityarangodb.Document, 0) num := -1.0 isNum := false @@ -99,7 +100,7 @@ func (s *Service) ExecuteQuery(query string, databaseUsername string, databasePa // Return nodes and edges queryResult["nodes"] = listContainer.NodeList queryResult["edges"] = listContainer.EdgeList - + queryResult["rows"] = listContainer.RowList jsonQ, err := json.Marshal(queryResult) return &jsonQ, err } @@ -111,11 +112,19 @@ func (s *Service) ExecuteQuery(query string, databaseUsername string, databasePa /* parseResult takes the result of the query and translates this to two lists: a nodelist and an edgelist, stored in a listcontainer doc: map[string]interface{}, the document with the nodes and vertices that came back from the database - listContainer: *entity.ListContainer, a struct containing the nodelist and edgelist that will be returned to the frontend + listContainer: *entityarangodb.ListContainer, a struct containing the nodelist and edgelist that will be returned to the frontend */ -func parseResult(doc map[string]interface{}, listContainer *entity.ListContainer) { - vertices := doc["vertices"].([]interface{}) - edges := doc["edges"].([]interface{}) +func parseResult(doc map[string]interface{}, listContainer *entityarangodb.ListContainer) { + var vertices []interface{} + var edges []interface{} + _, vertex := doc["vertices"] + _, edge := doc["edges"] + if vertex && edge { // Node link + vertices = doc["vertices"].([]interface{}) + edges = doc["edges"].([]interface{}) + } else { // Table + (*listContainer).RowList = append(((*listContainer).RowList), doc) + } for _, vertex := range vertices { if vertex != nil { @@ -137,10 +146,10 @@ func parseResult(doc map[string]interface{}, listContainer *entity.ListContainer /* parseEdge parses the data of an edge to an output-friendly format doc: map[string]interface{}, a single edge returned from the database - Returns: entity.Document, a document with almost the same structure as before, but the attributes are grouped + Returns: entityarangodb.Document, a document with almost the same structure as before, but the attributes are grouped */ -func parseEdge(doc map[string]interface{}) entity.Document { - data := make(entity.Document) +func parseEdge(doc map[string]interface{}) entityarangodb.Document { + data := make(entityarangodb.Document) data["id"] = doc["_id"] delete(doc, "_id") delete(doc, "_key") @@ -157,10 +166,10 @@ func parseEdge(doc map[string]interface{}) entity.Document { /* parseEdge parses the data of a node to an output-friendly format doc: map[string]interface{}, a single entry of an node - Returns: entity.Document, a document with almost the same structure as before, but the attributes are grouped + Returns: entityarangodb.Document, a document with almost the same structure as before, but the attributes are grouped */ -func parseNode(doc map[string]interface{}) entity.Document { - data := make(entity.Document) +func parseNode(doc map[string]interface{}) entityarangodb.Document { + data := make(entityarangodb.Document) data["id"] = doc["_id"] delete(doc, "_id") delete(doc, "_key") diff --git a/arangodb/executeQuery_test.go b/usecases/arangodb/executeQuery_test.go similarity index 83% rename from arangodb/executeQuery_test.go rename to usecases/arangodb/executeQuery_test.go index a48c5d0..a66e994 100644 --- a/arangodb/executeQuery_test.go +++ b/usecases/arangodb/executeQuery_test.go @@ -123,6 +123,59 @@ func TestValidQueries(t *testing.T) { RETURN {"vertices":nodes, "edges":edges }`, out: `{"edges":[{"_from":"airports/JFK","_id":"flights/1839","_key":"1839","_to":"airports/SFO","attributes":{"ArrTime":327,"ArrTimeUTC":"2008-01-01T11:27:00.000Z","Day":1,"DayOfWeek":2,"DepTime":11,"DepTimeUTC":"2008-01-01T05:11:00.000Z","Distance":2586,"FlightNum":9,"Month":1,"TailNum":"N555UA","UniqueCarrier":"UA","Year":2008}}],"nodes":[{"_id":"airports/SFO","_key":"SFO","attributes":{"city":"San Francisco","country":"USA","lat":37.61900194,"long":-122.3748433,"name":"San Francisco International","state":"CA","vip":true}},{"_id":"airports/JFK","_key":"JFK","attributes":{"city":"New York","country":"USA","lat":40.63975111,"long":-73.77892556,"name":"John F Kennedy Intl","state":"NY","vip":true}}]}`, }, + { + name: `group by query with table return`, + in: `LET tree_0 = ( + FOR e_9 IN parliament + LET e10 = ( + FOR e_10 IN commissions + FOR r8 IN part_of + FILTER r8._from == e_9._id AND r8._to == e_10._id + FILTER length(e_10) != 0 AND length(r8) != 0 + RETURN {"e10": union_distinct([e_10], []), "r8": union_distinct([r8], [])} + ) + FILTER length(e10) != 0 AND length(e_9) != 0 + RETURN {"e9": union_distinct([e_9], []), "e10": union_distinct(flatten(e10[**].e10), []), "r8": union_distinct(flatten(e10[**].r8), [])} + ) + LET gt13 = ( + FOR x IN part_of + LET variable_0 = ( + LET tmp = union_distinct(flatten(tree_0[**].e10), []) + FOR y IN tmp + FILTER y._id == x._to + RETURN y._id + ) + LET variable_1 = variable_0[0] + LET variable_2 = ( + LET tmp = union_distinct(flatten(tree_0[**].e9), []) + FOR y IN tmp + FILTER y._id == x._from + RETURN y.age + ) + LET variable_3 = variable_2[0] + FILTER variable_1 != NULL AND variable_3 != NULL + RETURN { + "group": variable_1, + "by": variable_3 + } + ) + LET g13 = ( + FOR x IN gt13 + COLLECT c = x.group INTO groups = x.by + LET variable_0 = AVG(groups) + FILTER variable_0 > 45 + RETURN { + _id: c, + modifier: variable_0 + } + ) + FOR x in g13 + RETURN {group: x.modifier, by: x._id}`, + out: `{"edges":[],"nodes":[],"rows":[{"by":"commissions/15","group":48.53846153846154},{"by":"commissions/20","group":55.333333333333336}, + {"by":"commissions/25","group":45.2},{"by":"commissions/26","group":46.25},{"by":"commissions/27","group":45.25},{"by":"commissions/28","group":55.4}, + {"by":"commissions/29","group":45.36363636363637},{"by":"commissions/3","group":46.4},{"by":"commissions/33","group":45.25},{"by":"commissions/35","group":47}, + {"by":"commissions/36","group":53.75},{"by":"commissions/4","group":46.15384615384615}]}`, + }, } for _, tt := range queryTests { diff --git a/arangodb/queryExecutor.go b/usecases/arangodb/queryExecutor.go similarity index 100% rename from arangodb/queryExecutor.go rename to usecases/arangodb/queryExecutor.go diff --git a/arangodb/test.sh b/usecases/arangodb/test.sh old mode 100755 new mode 100644 similarity index 100% rename from arangodb/test.sh rename to usecases/arangodb/test.sh diff --git a/neo4j/entity/types.go b/usecases/neo4j/entity/types.go similarity index 100% rename from neo4j/entity/types.go rename to usecases/neo4j/entity/types.go diff --git a/neo4j/executeQuery.go b/usecases/neo4j/executeQuery.go similarity index 98% rename from neo4j/executeQuery.go rename to usecases/neo4j/executeQuery.go index 4bca84c..f52e844 100644 --- a/neo4j/executeQuery.go +++ b/usecases/neo4j/executeQuery.go @@ -11,7 +11,7 @@ import ( "fmt" "strings" - entityneo4j "git.science.uu.nl/graphpolaris/query-execution/neo4j/entity" + entityneo4j "git.science.uu.nl/graphpolaris/query-execution/usecases/neo4j/entity" "github.com/neo4j/neo4j-go-driver/v4/neo4j" "github.com/neo4j/neo4j-go-driver/v4/neo4j/db" "github.com/neo4j/neo4j-go-driver/v4/neo4j/dbtype" diff --git a/neo4j/executeQuery_test.go b/usecases/neo4j/executeQuery_test.go similarity index 100% rename from neo4j/executeQuery_test.go rename to usecases/neo4j/executeQuery_test.go diff --git a/neo4j/queryExecutor.go b/usecases/neo4j/queryExecutor.go similarity index 100% rename from neo4j/queryExecutor.go rename to usecases/neo4j/queryExecutor.go diff --git a/usecases/sparql/entity/result.go b/usecases/sparql/entity/result.go new file mode 100644 index 0000000..9e0a370 --- /dev/null +++ b/usecases/sparql/entity/result.go @@ -0,0 +1,53 @@ +/* +This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course. +© Copyright Utrecht University (Department of Information and Computing Sciences) +*/ + +package entitysparql + +/* +Defines the result type of the JSON received as a response when sending a query to a SPARQL dataset +standard by https://www.w3.org/TR/2013/REC-sparql11-results-json-20130321/ +*/ +type Result struct { + Head Select + Results map[string][]map[string]Binding +} + +type Select struct { + Vars []string +} + +type Bindings struct { + Values interface{} +} + +type Binding struct { + Type string + Value string +} + +/* +Maps a relation (e.g. r1) to the two entities it's connceted to (e.g. e0,e1) +This is our neccesary metadata for executing a query and converting the output into our standard format +*/ +type MetaData struct { + Triples map[string]Tuple +} + +/* +Go has no tuples, so we make one ourselves specifically for SPARQL subject-predicate-object relation types +*/ +type Tuple struct { + From string + To string +} + +/* +This is only used for duplicate checking in a roundabout manner, since entity.Edge cannot be used as a key for a map, since it contains interface{} +*/ +type DuplicateCheckingTriple struct { + Rel string + From string + To string +} diff --git a/usecases/sparql/executeQuery.go b/usecases/sparql/executeQuery.go new file mode 100644 index 0000000..8414a54 --- /dev/null +++ b/usecases/sparql/executeQuery.go @@ -0,0 +1,197 @@ +/* +This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course. +© Copyright Utrecht University (Department of Information and Computing Sciences) +*/ + +package sparql + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + + "git.science.uu.nl/graphpolaris/query-execution/entity" + entitysparql "git.science.uu.nl/graphpolaris/query-execution/usecases/sparql/entity" +) + +/* +ExecuteQuery executes the given query on an ArangoDB instance +Currently DOES NOT actually implement the Executor interface, since it requires a different set of parameters +Since this code is essentially a proof-of-concept/template-to-be-extended, + there is no reason to mess with the current, and working, interface definition and its implementations + query: string, the query to be executed + endpoint: string, the public SPARQL endpoint to query + incomingTriplesJSON: string, the triples used in the query corresponding to their select identifiers + Return: (*[]byte, error), returns the result of the query and a potential error +*/ +func (s *Service) ExecuteQuery(query string, incomingTriplesJSON string, endpoint string) (*[]byte, error) { + + // TODO: Actually query a dataset rather than doing it from this mocked JSON file of data + // You can probably curl the endpoint, sample code is provided in the "curl" function + + // Create data structure to store query result in + // This means after converting the database response to our standard format + nodeList := make([]entity.Node, 0) + edgeList := make([]entity.Edge, 0) + queryResult := make(map[string]interface{}) + // Unmarshal metaData into struct, query service can also pass an object instead of string I think? + // TODO: Make query service send the new IncomingTranslation object rahter than strings + metaData := entitysparql.MetaData{} + err := json.Unmarshal([]byte(incomingTriplesJSON), &metaData) + if err != nil { + fmt.Println(err) + } + fmt.Println(metaData) + + // Just a dummy function that reads and unmarshals our mock data + // It goes into a entitysparql.Result format + result := openAndRead(endpoint) + // The ?e and ?r items get put into their respective lists + selectedEntities, selectedRelations, err := getSelects(&result) + if err != nil { + fmt.Println(err) + } + // Maps to avoid duplicates + nodeDoneMap := make(map[string]bool) + relDoneMap := make(map[entitysparql.DuplicateCheckingTriple]bool) + for i, binding := range result.Results["bindings"] { + // First just make the nodes + for _, e := range selectedEntities { + // Avoid putting in duplicate nodes + if _, ok := nodeDoneMap[binding[e].Value]; !ok { + node := entity.Node{} + attributes := make(map[string]interface{}) + attributes[binding[e].Type] = binding[e].Value + node.ID = binding[e].Value + node.Attributes = attributes + nodeList = append(nodeList, node) + nodeDoneMap[binding[e].Value] = true + } + } + // Then do relations + for j, r := range selectedRelations { + // Funny way of turning two digits into a unique number + id := fmt.Sprintf("%v%v", i, j) + if err != nil { + return nil, err + } + edge := entity.Edge{} + attributes := make(map[string]interface{}) + // This is basically giving the edge the "uri" attribute + attributes[binding[r].Type] = binding[r].Value + edge.ID = id + edge.Attributes = attributes + // metaData.Triples is our map that allows us to find the entities a relation is connected to + // That way we can get access to the ID (key[].Value) that we have to connect to + for _, potentialE := range selectedEntities { + if potentialE == metaData.Triples[r].From { + edge.From = binding[potentialE].Value + + } else if potentialE == metaData.Triples[r].To { + edge.To = binding[potentialE].Value + } + } + // We can't use a map[edge]bool so we create this quick triple to check if a specific relation is already present + // If it is, we can skip it + // Not very clean code, but since go has no generics this will have to do + // If we had generics then attributes wouldn't be interface{} and it would be possible to use it as a key for a map + tempTriple := entitysparql.DuplicateCheckingTriple{ + From: edge.From, + To: edge.To, + Rel: binding[r].Value, + } + // Only add the result if we don't already have it + if _, ok := relDoneMap[tempTriple]; !ok { + edgeList = append(edgeList, edge) + relDoneMap[tempTriple] = true + } + + } + } + // Doesn't add rows for GroupBy at the moment + queryResult["nodes"] = nodeList + queryResult["edges"] = edgeList + jsonQ, err := json.Marshal(queryResult) + return &jsonQ, err + +} + +/* +curl sends a http POST request to a SPARQL endpoint and decodes the JSON response into a struct + query: string, the query to be executed + endpoint: string, the public SPARQL endpoint to query + target: interface{}, the struct that should be filled with JSON data, this parameter is mutated during the function + Return: error, returns either a http or JSON related error +*/ +func curl(query string, endpoint string, target interface{}) error { + // you can run this curl command to query an endpoint + // curl -X POST http://35.233.212.30/blazegraph/namespace/kb/sparql --data "query=SELECT * { ?s ?p ?o } LIMIT 10" --data "format=json" + // or in more general format + // curl -X POST endpoint --data query --data "format=json" + + params := url.Values{} + params.Add("query", query) + params.Add("format", `json`) + body := strings.NewReader(params.Encode()) + + req, err := http.NewRequest("POST", endpoint, body) + if err != nil { + // handle err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + // handle err + } + defer resp.Body.Close() + return json.NewDecoder(resp.Body).Decode(target) + +} + +/* +getSelects splits the items preceded by the SELECT statement into two lists, one for entities and one for relations + queryResult: *entitysparql.Result, contains all selected variables + Return: ([]string, []string, error), the first list contains all entity variable names, the second list contains all relation variable names, the error in case a variable name is not recognized +*/ +func getSelects(queryResult *entitysparql.Result) ([]string, []string, error) { + selectedEntities := make([]string, 0) + selectedRelations := make([]string, 0) + for _, selectedItem := range queryResult.Head.Vars { + if rune(selectedItem[0]) == 'e' { + selectedEntities = append(selectedEntities, selectedItem) + } else if rune(selectedItem[0]) == 'r' { + selectedRelations = append(selectedRelations, selectedItem) + } else { + return selectedEntities, selectedRelations, errors.New("unrecognized var name") + } + } + return selectedEntities, selectedRelations, nil +} + +/* +openAndRead reads the mock data from a JSON file + Return: entitysparql.Result, the formatted results from the database +*/ +func openAndRead(file string) entitysparql.Result { + jsonFile, err := os.Open(file) + // if we os.Open returns an error then handle it + if err != nil { + fmt.Println(err) + } + // defer the closing of our jsonFile so that we can parse it later on + defer jsonFile.Close() + // read our opened jsonFile as a byte array. + var result entitysparql.Result + byteValue, _ := ioutil.ReadAll(jsonFile) + err = json.Unmarshal(byteValue, &result) + if err != nil { + fmt.Println(err) + } + return result +} diff --git a/usecases/sparql/executeQuery_test.go b/usecases/sparql/executeQuery_test.go new file mode 100644 index 0000000..407729e --- /dev/null +++ b/usecases/sparql/executeQuery_test.go @@ -0,0 +1,46 @@ +/* +This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course. +© Copyright Utrecht University (Department of Information and Computing Sciences) +*/ + +package sparql + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var s *Service + +/* +Creates the service +*/ +func createService() { + if s != nil { + return + } + s = NewService() +} + +/* +Tests if the executor properly runs using the mock data + t: *testing.T, makes go recognize this as a test +*/ +func TestExecution(t *testing.T) { + createService() + triples := `{ + "triples": { + "r0": { + "from": "e0", + "to": "e1" + }, + "r1": { + "from": "e2", + "to": "e1" + } + } + }` + _, err := s.ExecuteQuery("mock-query", triples, "../../main/sparql-mock-result.json") + assert.Nil(t, err) +} diff --git a/usecases/sparql/interface.go b/usecases/sparql/interface.go new file mode 100644 index 0000000..09dbc8f --- /dev/null +++ b/usecases/sparql/interface.go @@ -0,0 +1,21 @@ +/* +This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course. +© Copyright Utrecht University (Department of Information and Computing Sciences) +*/ + +package sparql + +/* +Service SHOULD implement the Executor interface +It currently does NOT +*/ +type Service struct { +} + +/* +NewService creates a new ArangoDB executor service + Return: *Service, the new service +*/ +func NewService() *Service { + return &Service{} +} -- GitLab