diff --git a/client_test.go b/client_test.go index 85effbe..241918c 100644 --- a/client_test.go +++ b/client_test.go @@ -16,8 +16,8 @@ func createGraph() { graph = GraphNew("social", conn) // Create 2 nodes connect via a single edge. - japan := NodeNew("Country", "j", nil) - john := NodeNew("Person", "p", nil) + japan := NodeNew([]string{"Country"}, "j", nil) + john := NodeNew([]string{"Person"}, "p", nil) edge := EdgeNew("Visited", john, japan, nil) // Set node properties. @@ -91,9 +91,9 @@ func checkQueryResults(t *testing.T, res *QueryResult) { d, ok := r.GetByIndex(2).(*Node) assert.True(t, ok, "Third column should contain nodes.") - assert.Equal(t, s.Label, "Person", "Node should be of type 'Person'") + assert.Equal(t, s.Labels[0], "Person", "Node should be of type 'Person'") assert.Equal(t, e.Relation, "Visited", "Edge should be of relation type 'Visited'") - assert.Equal(t, d.Label, "Country", "Node should be of type 'Country'") + assert.Equal(t, d.Labels[0], "Country", "Node should be of type 'Country'") assert.Equal(t, len(s.Properties), 4, "Person node should have 4 properties") @@ -131,7 +131,7 @@ func TestCreateQuery(t *testing.T) { res.Next() r := res.Record() w := r.GetByIndex(0).(*Node) - assert.Equal(t, w.Label, "WorkPlace", "Unexpected node label.") + assert.Equal(t, w.Labels[0], "WorkPlace", "Unexpected node label.") } func TestCreateROQueryFailure(t *testing.T) { @@ -199,8 +199,8 @@ func TestArray(t *testing.T) { t.Error(err) } - a := NodeNew("person", "", nil) - b := NodeNew("person", "", nil) + a := NodeNew([]string{"person"}, "", nil) + b := NodeNew([]string{"person"}, "", nil) a.SetProperty("name", "a") a.SetProperty("age", 32) @@ -288,9 +288,9 @@ func TestPath(t *testing.T) { e := p.GetEdge(0) d := p.LastNode() - assert.Equal(t, s.Label, "Person", "Node should be of type 'Person'") + assert.Equal(t, s.Labels[0], "Person", "Node should be of type 'Person'") assert.Equal(t, e.Relation, "Visited", "Edge should be of relation type 'Visited'") - assert.Equal(t, d.Label, "Country", "Node should be of type 'Country'") + assert.Equal(t, d.Labels[0], "Country", "Node should be of type 'Country'") assert.Equal(t, len(s.Properties), 4, "Person node should have 4 properties") @@ -308,12 +308,12 @@ func TestPath(t *testing.T) { func TestParameterizedQuery(t *testing.T) { createGraph() - params := []interface{}{1, 2.3, "str", true, false, nil, []interface {}{0, 1, 2}, []interface {}{"0", "1", "2"}} + params := []interface{}{1, 2.3, "str", true, false, nil, []interface{}{0, 1, 2}, []interface{}{"0", "1", "2"}} q := "RETURN $param" params_map := make(map[string]interface{}) for index, param := range params { params_map["param"] = param - res, err := graph.ParameterizedQuery(q, params_map); + res, err := graph.ParameterizedQuery(q, params_map) if err != nil { t.Error(err) } @@ -348,24 +348,24 @@ func TestCreateIndex(t *testing.T) { func TestQueryStatistics(t *testing.T) { graph.Flush() err := graph.Delete() - assert.Nil(t,err) + assert.Nil(t, err) q := "CREATE (:Person{name:'a',age:32,array:[0,1,2]})" res, err := graph.Query(q) - assert.Nil(t,err) + assert.Nil(t, err) assert.Equal(t, 1, res.NodesCreated(), "Expecting 1 node created") assert.Equal(t, 0, res.NodesDeleted(), "Expecting 0 nodes deleted") - assert.Greater(t, res.InternalExecutionTime(),0.0, "Expecting internal execution time not to be 0.0") + assert.Greater(t, res.InternalExecutionTime(), 0.0, "Expecting internal execution time not to be 0.0") assert.Equal(t, true, res.Empty(), "Expecting empty resultset") - res,err = graph.Query("MATCH (n) DELETE n") - assert.Nil(t,err) + res, err = graph.Query("MATCH (n) DELETE n") + assert.Nil(t, err) assert.Equal(t, 1, res.NodesDeleted(), "Expecting 1 nodes deleted") // Create 2 nodes connect via a single edge. - japan := NodeNew("Country", "j", nil) - john := NodeNew("Person", "p", nil) + japan := NodeNew([]string{"Country"}, "j", nil) + john := NodeNew([]string{"Person"}, "p", nil) edge := EdgeNew("Visited", john, japan, nil) // Set node properties. @@ -386,21 +386,21 @@ func TestQueryStatistics(t *testing.T) { // Flush graph to DB. res, err = graph.Commit() - assert.Nil(t,err) + assert.Nil(t, err) assert.Equal(t, 2, res.NodesCreated(), "Expecting 2 node created") assert.Equal(t, 0, res.NodesDeleted(), "Expecting 0 nodes deleted") assert.Equal(t, 7, res.PropertiesSet(), "Expecting 7 properties set") assert.Equal(t, 1, res.RelationshipsCreated(), "Expecting 1 relationships created") assert.Equal(t, 0, res.RelationshipsDeleted(), "Expecting 0 relationships deleted") - assert.Greater(t, res.InternalExecutionTime(),0.0, "Expecting internal execution time not to be 0.0") + assert.Greater(t, res.InternalExecutionTime(), 0.0, "Expecting internal execution time not to be 0.0") assert.Equal(t, true, res.Empty(), "Expecting empty resultset") q = "MATCH p = (:Person)-[:Visited]->(:Country) RETURN p" res, err = graph.Query(q) - assert.Nil(t,err) + assert.Nil(t, err) assert.Equal(t, len(res.results), 1, "expecting 1 result record") assert.Equal(t, false, res.Empty(), "Expecting resultset to have records") - res,err = graph.Query("MATCH ()-[r]-() DELETE r") - assert.Nil(t,err) + res, err = graph.Query("MATCH ()-[r]-() DELETE r") + assert.Nil(t, err) assert.Equal(t, 1, res.RelationshipsDeleted(), "Expecting 1 relationships deleted") } @@ -410,9 +410,9 @@ func TestUtils(t *testing.T) { res = ToString("test_string") assert.Equal(t, res, "\"test_string\"") - + res = ToString(10) - assert.Equal(t, res, "10") + assert.Equal(t, res, "10") res = ToString(1.2) assert.Equal(t, res, "1.2") @@ -420,29 +420,55 @@ func TestUtils(t *testing.T) { res = ToString(true) assert.Equal(t, res, "true") - var arr = []interface{}{1,2,3,"boom"} + var arr = []interface{}{1, 2, 3, "boom"} res = ToString(arr) assert.Equal(t, res, "[1,2,3,\"boom\"]") - + jsonMap := make(map[string]interface{}) - jsonMap["object"] = map[string]interface{} {"foo": 1} + jsonMap["object"] = map[string]interface{}{"foo": 1} res = ToString(jsonMap) assert.Equal(t, res, "{object: {foo: 1}}") } +func TestMultiLabelNode(t *testing.T) { + // clear database + graph.Flush() + err := graph.Delete() + assert.Nil(t, err) + + // create a multi label node + multiLabelNode := NodeNew([]string{"A","B"}, "n", nil) + graph.AddNode(multiLabelNode) + _, err = graph.Commit() + assert.Nil(t, err) + + // fetch node + res, err := graph.Query("MATCH (n) RETURN n") + assert.Nil(t, err) + + res.Next() + r := res.Record() + n := r.GetByIndex(0).(*Node) + + // expecting 2 labels + assert.Equal(t, len(n.Labels), 2, "expecting 2 labels") + assert.Equal(t, n.Labels[0], "A") + assert.Equal(t, n.Labels[1], "B") +} + func TestNodeMapDatatype(t *testing.T) { graph.Flush() err := graph.Delete() assert.Nil(t, err) // Create 2 nodes connect via a single edge. - japan := NodeNew("Country", "j", + japan := NodeNew([]string{"Country"}, "j", map[string]interface{}{ "name": "Japan", "population": 126800000, "states": []string{"Kanto", "Chugoku"}, }) - john := NodeNew("Person", "p", + john := NodeNew([]string{"Person"}, "p", map[string]interface{}{ "name": "John Doe", "age": 33, @@ -488,7 +514,7 @@ func TestTimeout(t *testing.T) { params := make(map[string]interface{}) params["ub"] = 1000000 - res, err = graph.ParameterizedQueryWithOptions("UNWIND range(0, $ub) AS v RETURN v", params, options); + res, err = graph.ParameterizedQueryWithOptions("UNWIND range(0, $ub) AS v RETURN v", params, options) assert.Nil(t, res) assert.NotNil(t, err) } diff --git a/edge.go b/edge.go index 95b79f8..724dbc7 100644 --- a/edge.go +++ b/edge.go @@ -17,6 +17,7 @@ type Edge struct { graph *Graph } +// EdgeNew create a new Edge func EdgeNew(relation string, srcNode *Node, destNode *Node, properties map[string]interface{}) *Edge { p := properties if p == nil { @@ -32,15 +33,18 @@ func EdgeNew(relation string, srcNode *Node, destNode *Node, properties map[stri } } +// SetProperty assign a new property to edge func (e *Edge) SetProperty(key string, value interface{}) { e.Properties[key] = value } +// GetProperty retrieves property from edge func (e *Edge) GetProperty(key string) interface{} { v, _ := e.Properties[key] return v } +// SourceNodeID returns edge source node ID func (e Edge) SourceNodeID() uint64 { if e.Source != nil { return e.Source.ID @@ -49,6 +53,7 @@ func (e Edge) SourceNodeID() uint64 { } } +// DestNodeID returns edge destination node ID func (e Edge) DestNodeID() uint64 { if e.Source != nil { return e.Destination.ID @@ -57,6 +62,7 @@ func (e Edge) DestNodeID() uint64 { } } +// Returns a string representation of edge func (e Edge) String() string { if len(e.Properties) == 0 { return "{}" @@ -71,6 +77,7 @@ func (e Edge) String() string { return s } +// Encode makes Edge satisfy the Stringer interface func (e Edge) Encode() string { s := []string{"(", e.Source.Alias, ")"} diff --git a/example_graph_test.go b/example_graph_test.go index c475ee3..7bb1876 100644 --- a/example_graph_test.go +++ b/example_graph_test.go @@ -4,11 +4,12 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "github.com/RedisGraph/redisgraph-go" - "github.com/gomodule/redigo/redis" "io/ioutil" "log" "os" + + "github.com/RedisGraph/redisgraph-go" + "github.com/gomodule/redigo/redis" ) func ExampleGraphNew() { @@ -22,7 +23,7 @@ func ExampleGraphNew() { res.Next() r := res.Record() w := r.GetByIndex(0).(*redisgraph.Node) - fmt.Println(w.Label) + fmt.Println(w.Labels[0]) // Output: WorkPlace } @@ -40,7 +41,7 @@ func ExampleGraphNew_pool() { res.Next() r := res.Record() w := r.GetByIndex(0).(*redisgraph.Node) - fmt.Println(w.Label) + fmt.Println(w.Labels[0]) // Output: WorkPlace } @@ -103,7 +104,7 @@ func ExampleGraphNew_tls() { res.Next() r := res.Record() w := r.GetByIndex(0).(*redisgraph.Node) - fmt.Println(w.Label) + fmt.Println(w.Labels[0]) } func getConnectionDetails() (host string, password string) { diff --git a/examples/redisgraph_tls_client/redisgraph_tls_client.go b/examples/redisgraph_tls_client/redisgraph_tls_client.go index 769b7d3..f3b03b0 100644 --- a/examples/redisgraph_tls_client/redisgraph_tls_client.go +++ b/examples/redisgraph_tls_client/redisgraph_tls_client.go @@ -5,11 +5,12 @@ import ( "crypto/x509" "flag" "fmt" - "github.com/RedisGraph/redisgraph-go" - "github.com/gomodule/redigo/redis" "io/ioutil" "log" "os" + + "github.com/RedisGraph/redisgraph-go" + "github.com/gomodule/redigo/redis" ) var ( @@ -85,6 +86,6 @@ func main() { res.Next() r := res.Record() w := r.GetByIndex(0).(*redisgraph.Node) - fmt.Println(w.Label) + fmt.Println(w.Labels[0]) // Output: WorkPlace } diff --git a/graph.go b/graph.go index bc3e6b9..0cc8069 100644 --- a/graph.go +++ b/graph.go @@ -75,6 +75,12 @@ func (g *Graph) ExecutionPlan(q string) (string, error) { // Delete removes the graph. func (g *Graph) Delete() error { _, err := g.Conn.Do("GRAPH.DELETE", g.Id) + + // clear internal mappings + g.labels = g.labels[:0] + g.properties = g.properties[:0] + g.relationshipTypes = g.relationshipTypes[:0] + return err } @@ -266,7 +272,7 @@ func (g *Graph) CallProcedure(procedure string, yield []string, args ...interfac } q += fmt.Sprintf("%s)", strings.Join(tmp, ",")) - if yield != nil && len(yield) > 0 { + if len(yield) > 0 { q += fmt.Sprintf(" YIELD %s", strings.Join(yield, ",")) } diff --git a/node.go b/node.go index 50f7308..b6f6f81 100644 --- a/node.go +++ b/node.go @@ -5,16 +5,17 @@ import ( "strings" ) -// Node represents a node within a graph. +// Node represents a node within a graph type Node struct { ID uint64 - Label string + Labels []string Alias string Properties map[string]interface{} graph *Graph } -func NodeNew(label string, alias string, properties map[string]interface{}) *Node { +// NodeNew create a new Node +func NodeNew(labels []string, alias string, properties map[string]interface{}) *Node { p := properties if p == nil { @@ -22,22 +23,25 @@ func NodeNew(label string, alias string, properties map[string]interface{}) *Nod } return &Node{ - Label: label, + Labels: labels, Alias: alias, Properties: p, graph: nil, } } +// SetProperty asssign a new property to node func (n *Node) SetProperty(key string, value interface{}) { n.Properties[key] = value } +// GetProperty retrieves property from node func (n Node) GetProperty(key string) interface{} { v, _ := n.Properties[key] return v } +// Returns a string representation of a node func (n Node) String() string { if len(n.Properties) == 0 { return "{}" @@ -52,7 +56,7 @@ func (n Node) String() string { return s } -// String makes Node satisfy the Stringer interface. +// Encode makes Node satisfy the Stringer interface func (n Node) Encode() string { s := []string{"("} @@ -60,8 +64,8 @@ func (n Node) Encode() string { s = append(s, n.Alias) } - if n.Label != "" { - s = append(s, ":", n.Label) + for _, label := range n.Labels { + s = append(s, ":", label) } if len(n.Properties) > 0 { diff --git a/query_result.go b/query_result.go index f16fe4b..fe82e06 100644 --- a/query_result.go +++ b/query_result.go @@ -5,6 +5,7 @@ import ( "os" "strconv" "strings" + "github.com/gomodule/redigo/redis" "github.com/olekukonko/tablewriter" ) @@ -16,10 +17,10 @@ const ( RELATIONSHIPS_DELETED string = "Relationships deleted" PROPERTIES_SET string = "Properties set" RELATIONSHIPS_CREATED string = "Relationships created" - INDICES_CREATED string = "Indices created" - INDICES_DELETED string = "Indices deleted" + INDICES_CREATED string = "Indices created" + INDICES_DELETED string = "Indices deleted" INTERNAL_EXECUTION_TIME string = "Query internal execution time" - CACHED_EXECUTION string = "Cached execution" + CACHED_EXECUTION string = "Cached execution" ) type ResultSetColumnTypes int @@ -54,11 +55,11 @@ type QueryResultHeader struct { // QueryResult represents the results of a query. type QueryResult struct { - graph *Graph - header QueryResultHeader - results []*Record - statistics map[string]float64 - current_record_idx int + graph *Graph + header QueryResultHeader + results []*Record + statistics map[string]float64 + currentRecordIdx int } func QueryResultNew(g *Graph, response interface{}) (*QueryResult, error) { @@ -69,8 +70,8 @@ func QueryResultNew(g *Graph, response interface{}) (*QueryResult, error) { column_names: make([]string, 0), column_types: make([]ResultSetColumnTypes, 0), }, - graph: g, - current_record_idx: -1, + graph: g, + currentRecordIdx: -1, } r, _ := redis.Values(response, nil) @@ -138,13 +139,10 @@ func (qr *QueryResult) parseRecords(raw_result_set []interface{}) { case COLUMN_SCALAR: s, _ := redis.Values(c, nil) values[idx] = qr.parseScalar(s) - break case COLUMN_NODE: values[idx] = qr.parseNode(c) - break case COLUMN_RELATION: values[idx] = qr.parseEdge(c) - break default: panic("Unknown column type.") } @@ -172,18 +170,18 @@ func (qr *QueryResult) parseNode(cell interface{}) *Node { // [label string offset (integer)], // [[name, value type, value] X N] - var label string c, _ := redis.Values(cell, nil) id, _ := redis.Uint64(c[0], nil) - labels, _ := redis.Ints(c[1], nil) - if len(labels) > 0 { - label = qr.graph.getLabel(labels[0]) + labelIds, _ := redis.Ints(c[1], nil) + labels := make([]string, len(labelIds)) + for i := 0; i < len(labelIds); i++ { + labels[i] = qr.graph.getLabel(labelIds[i]) } rawProps, _ := redis.Values(c[2], nil) properties := qr.parseProperties(rawProps) - n := NodeNew(label, "", properties) + n := NodeNew(labels, "", properties) n.ID = id return n } @@ -296,8 +294,8 @@ func (qr *QueryResult) Next() bool { if qr.Empty() { return false } - if qr.current_record_idx < len(qr.results)-1 { - qr.current_record_idx++ + if qr.currentRecordIdx < len(qr.results)-1 { + qr.currentRecordIdx++ return true } else { return false @@ -306,8 +304,8 @@ func (qr *QueryResult) Next() bool { // Record returns the current record. func (qr *QueryResult) Record() *Record { - if qr.current_record_idx >= 0 && qr.current_record_idx < len(qr.results) { - return qr.results[qr.current_record_idx] + if qr.currentRecordIdx >= 0 && qr.currentRecordIdx < len(qr.results) { + return qr.results[qr.currentRecordIdx] } else { return nil } @@ -386,4 +384,3 @@ func (qr *QueryResult) InternalExecutionTime() float64 { func (qr *QueryResult) CachedExecution() int { return int(qr.getStat(CACHED_EXECUTION)) } -