From f2edc9e8b7914e929d7bde14990f8a0445444f55 Mon Sep 17 00:00:00 2001 From: swilly22 Date: Wed, 24 Apr 2019 16:24:32 +0300 Subject: [PATCH 1/2] new result-set structure --- client.go | 285 ------------------------------------------------ client_test.go | 115 ++++++------------- edge.go | 85 +++++++++++++++ graph.go | 216 ++++++++++++++++++++++++++++++++++++ node.go | 81 ++++++++++++++ query_result.go | 244 +++++++++++++++++++++++++++++++++++++++++ utils.go | 48 ++++++++ 7 files changed, 710 insertions(+), 364 deletions(-) delete mode 100644 client.go create mode 100644 edge.go create mode 100644 graph.go create mode 100644 node.go create mode 100644 query_result.go create mode 100644 utils.go diff --git a/client.go b/client.go deleted file mode 100644 index 7790ceb..0000000 --- a/client.go +++ /dev/null @@ -1,285 +0,0 @@ -package redisgraph - -import ( - "crypto/rand" - "fmt" - "os" - "strings" - "strconv" - - - "github.com/gomodule/redigo/redis" - "github.com/olekukonko/tablewriter" -) - -func quoteString(i interface{}) interface{} { - switch x := i.(type) { - case string: - if len(x) == 0 { - return "\"\"" - } - if x[0] != '"' { - x = "\"" + x - } - if x[len(x)-1] != '"' { - x += "\"" - } - return x - default: - return i - } -} - -// https://medium.com/@kpbird/golang-generate-fixed-size-random-string-dd6dbd5e63c0 -func randomString(n int) string { - const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - - output := make([]byte, n) - // We will take n bytes, one byte for each character of output. - randomness := make([]byte, n) - // read all random - _, err := rand.Read(randomness) - if err != nil { - panic(err) - } - l := len(letterBytes) - // fill output - for pos := range output { - // get random item - random := uint8(randomness[pos]) - // random % 64 - randomPos := random % uint8(l) - // put into output - output[pos] = letterBytes[randomPos] - } - return string(output) -} - -// Node represents a node within a graph. -type Node struct { - ID string - Alias string - Label string - Properties map[string]interface{} -} - -func (n *Node) String() string { - s := []string{"("} - if n.Alias != "" { - s = append(s, n.Alias) - } - if n.Label != "" { - s = append(s, ":", n.Label) - } - if len(n.Properties) > 0 { - p := make([]string, 0, len(n.Properties)) - for k, v := range n.Properties { - p = append(p, fmt.Sprintf("%s:%v", k, quoteString(v))) - } - s = append(s, "{") - s = append(s, strings.Join(p, ",")) - s = append(s, "}") - } - s = append(s, ")") - return strings.Join(s, "") -} - -// Edge represents an edge connecting two nodes in the graph. -type Edge struct { - Source *Node - Destination *Node - Relation string - Properties map[string]interface{} -} - -func (e *Edge) String() string { - s := []string{"(", e.Source.Alias, ")"} - - s = append(s, "-[") - if e.Relation != "" { - s = append(s, ":", e.Relation) - } - - if len(e.Properties) > 0 { - p := make([]string, 0, len(e.Properties)) - for k, v := range e.Properties { - p = append(p, fmt.Sprintf("%s:%v", k, quoteString(v))) - } - s = append(s, strings.Join(p, ",")) - s = append(s, "{") - s = append(s, p...) - s = append(s, "}") - } - s = append(s, "]->") - - s = append(s, "(", e.Destination.Alias, ")") - - return strings.Join(s, "") -} - -// Graph represents a graph, which is a collection of nodes and edges. -type Graph struct { - Name string - Nodes map[string]*Node - Edges []*Edge - Conn redis.Conn -} - -// New creates a new graph. -func (g Graph) New(name string, conn redis.Conn) Graph { - r := Graph{ - Name: name, - Nodes: make(map[string]*Node), - Conn: conn, - } - return r -} - -// AddNode adds a node to the graph. -func (g *Graph) AddNode(n *Node) error { - if n.Alias == "" { - n.Alias = randomString(10) - } - g.Nodes[n.Alias] = n - return nil -} - -// AddEdge adds an edge to the graph. -func (g *Graph) AddEdge(e *Edge) error { - // Verify that the edge has source and destination - if e.Source == nil || e.Destination == nil { - return fmt.Errorf("AddEdge: both source and destination nodes should be defined") - } - - // Verify that the edge's nodes have been previously added to the graph - if _, ok := g.Nodes[e.Source.Alias]; !ok { - return fmt.Errorf("AddEdge: source node neeeds to be added to the graph first") - } - if _, ok := g.Nodes[e.Destination.Alias]; !ok { - return fmt.Errorf("AddEdge: destination node neeeds to be added to the graph first") - } - - g.Edges = append(g.Edges, e) - return nil -} - -// Commit creates the entire graph, but will readd nodes if called again. -func (g *Graph) Commit() (QueryResult, error) { - items := make([]string, 0, len(g.Nodes)+len(g.Edges)) - for _, n := range g.Nodes { - items = append(items, n.String()) - } - for _, e := range g.Edges { - items = append(items, e.String()) - } - q := "CREATE " + strings.Join(items, ",") - return g.Query(q) -} - -// Flush will create the graph and clear it -func (g *Graph) Flush() (QueryResult, error) { - res, err := g.Commit() - if err == nil { - g.Nodes = make(map[string]*Node) - g.Edges = make([]*Edge, 0) - } - return res, err -} - -// Query executes a query against the graph. -func (g *Graph) Query(q string) (QueryResult, error) { - qr := QueryResult{} - r, err := redis.Values(g.Conn.Do("GRAPH.QUERY", g.Name, q)) - if err != nil { - return qr, err - } - - // Result-set is an array of arrays. - results, err := redis.Values(r[0], nil) - if err != nil { - return qr, err - } - - records := make([][]interface{}, len(results)) - - for i, result := range results { - // Parse each record. - records[i], err = redis.Values(result, nil) - if err != nil { - return qr, err - } - } - - // Convert each record item to string. - qr.Results = make([][]string, len(records)) - for i, record := range records { - qr.Results[i] = make([]string, len(record)) - for j, item := range record { - switch item.(type) { - case int64: - n, err := redis.Int64(item, nil) - if err != nil { - return qr, err - } - qr.Results[i][j] = strconv.FormatInt(n, 10) - break - - case string: - qr.Results[i][j], err = redis.String(item, nil) - break - - default: - qr.Results[i][j], err = redis.String(item, nil) - break - } - } - } - - qr.Statistics, err = redis.Strings(r[1], nil) - if err != nil { - return qr, err - } - - return qr, nil -} - -// ExecutionPlan gets the execution plan for given query. -func (g *Graph) ExecutionPlan(q string) (string, error) { - return redis.String(g.Conn.Do("GRAPH.EXPLAIN", g.Name, q)) -} - -func (g *Graph) Delete() error { - _, err := g.Conn.Do("GRAPH.DELETE", g.Name) - return err -} - -// QueryResult represents the results of a query. -type QueryResult struct { - Results [][]string - Statistics []string -} - -func (qr *QueryResult) isEmpty() bool { - return len(qr.Results) == 0 -} - -// PrettyPrint prints the QueryResult to stdout, pretty-like. -func (qr *QueryResult) PrettyPrint() { - if !qr.isEmpty() { - table := tablewriter.NewWriter(os.Stdout) - table.SetAutoFormatHeaders(false) - table.SetHeader(qr.Results[0]) - if len(qr.Results) > 1 { - table.AppendBulk(qr.Results[1:]) - } else { - table.Append([]string{"No data returned."}) - } - table.Render() - } - - for _, stat := range qr.Statistics { - fmt.Fprintf(os.Stdout, "\n%s", stat) - } - - fmt.Fprintf(os.Stdout, "\n") -} diff --git a/client_test.go b/client_test.go index 587f4b5..a2cad33 100644 --- a/client_test.go +++ b/client_test.go @@ -1,98 +1,55 @@ package redisgraph import ( - "fmt" "testing" "github.com/gomodule/redigo/redis" ) -func TestExample(t *testing.T) { +func TestGraphCreation(t *testing.T) { + // Setup. conn, _ := redis.Dial("tcp", "0.0.0.0:6379") defer conn.Close() conn.Do("FLUSHALL") - rg := Graph{}.New("social", conn) - - john := Node{ - Label: "person", - Properties: map[string]interface{}{ - "name": "John Doe", - "age": 33, - "gender": "male", - "status": "single", - }, - } - err := rg.AddNode(&john) + rg := GraphNew("social", conn) + + // Create 2 nodes connect via a single edge. + japan := NodeNew(0, "country", "j", nil) + john := NodeNew(0, "person", "p", nil) + edge := EdgeNew(0, "visited", john, japan, nil) + + // Set node properties. + john.SetProperty("name", "John Doe") + john.SetProperty("age", 33) + john.SetProperty("gender", "male") + john.SetProperty("status", "single") + + // Introduce entities to graph. + rg.AddNode(john) + rg.AddNode(japan) + rg.AddEdge(edge) + + // Flush graph to DB. + resp, err := rg.Commit() if err != nil { t.Error(err) } - japan := Node{ - Label: "country", - Properties: map[string]interface{}{ - "name": "Japan", - }, - } - err = rg.AddNode(&japan) - if err != nil { - t.Error(err) + // Validate response. + if(resp.results != nil) { + t.FailNow() } - - edge := Edge{ - Source: &john, - Relation: "visited", - Destination: &japan, + if(resp.statistics["Labels added"] != 2) { + t.FailNow() } - err = rg.AddEdge(&edge) - if err != nil { - t.Error(err) - } - - _, err = rg.Commit() - if err != nil { - t.Error(err) - } - - query := `MATCH (p:person)-[v:visited]->(c:country) - RETURN p.name, p.age, v.purpose, c.name` - rs, err := rg.Query(query) - if err != nil { - t.Error(err) - } - - rs.PrettyPrint() -} - -func TestFlush(t *testing.T) { - conn, _ := redis.Dial("tcp", "0.0.0.0:6379") - defer conn.Close() - conn.Do("FLUSHALL") - rg := Graph{}.New("rubbles", conn) - users := [3]string{"Barney", "Betty", "Bam-Bam"} - for _, user := range users { - family := Node{ - Label: "person", - Properties: map[string]interface{}{ - "name": fmt.Sprintf("%s Rubble", user), - }, - } - err := rg.AddNode(&family) - if err != nil { - t.Error(err) - } - _, err = rg.Flush() - if err != nil { - t.Error(err) - } - } - query := `MATCH (p:person) RETURN p.name` - rs, err := rg.Query(query) - if err != nil { - t.Error(err) - } - if len(rs.Results) > 4 { - t.Errorf("There Should only be 4 entries but we get: %d", len(rs.Results)) - } - + if(resp.statistics["Nodes created"] != 2) { + t.FailNow() + } + if(resp.statistics["Properties set"] != 4) { + t.FailNow() + } + if(resp.statistics["Relationships created"] != 1) { + t.FailNow() + } } diff --git a/edge.go b/edge.go new file mode 100644 index 0000000..68edd17 --- /dev/null +++ b/edge.go @@ -0,0 +1,85 @@ +package redisgraph + +import ( + "fmt" + "strings" +) + +// Edge represents an edge connecting two nodes in the graph. +type Edge struct { + ID uint64 + Relation string + Source *Node + Destination *Node + Properties map[string]interface{} + srcNodeID uint64 + destNodeID uint64 + graph *Graph +} + +func EdgeNew(id uint64, relation string, srcNode *Node, destNode *Node, properties map[string]interface{}) *Edge { + return &Edge{ + ID:id, + Relation: relation, + Source: srcNode, + Destination: destNode, + Properties: properties, + graph: nil, + } +} + +func (e Edge) SourceNodeID() uint64 { + if(e.Source != nil) { + return e.Source.ID + } else { + return e.srcNodeID + } +} + +func (e Edge) DestNodeID() uint64 { + if(e.Source != nil) { + return e.Destination.ID + } else { + return e.destNodeID + } +} + +func (e Edge) String() string { + if len(e.Properties) == 0 { + return "{}" + } + + p := make([]string, 0, len(e.Properties)) + for k, v := range e.Properties { + p = append(p, fmt.Sprintf("%s:%v", k, QuoteString(v))) + } + + s := fmt.Sprintf("{%s}", strings.Join(p, ",")) + return s +} + +func (e Edge) Encode() string { + s := []string{"(", e.Source.Alias, ")"} + + s = append(s, "-[") + + if e.Relation != "" { + s = append(s, ":", e.Relation) + } + + if len(e.Properties) > 0 { + p := make([]string, 0, len(e.Properties)) + for k, v := range e.Properties { + p = append(p, fmt.Sprintf("%s:%v", k, QuoteString(v))) + } + + s = append(s, "{") + s = append(s, strings.Join(p, ",")) + s = append(s, "}") + } + + s = append(s, "]->") + s = append(s, "(", e.Destination.Alias, ")") + + return strings.Join(s, "") +} diff --git a/graph.go b/graph.go new file mode 100644 index 0000000..d65371e --- /dev/null +++ b/graph.go @@ -0,0 +1,216 @@ +package redisgraph + +import ( + "fmt" + "strings" + + "github.com/gomodule/redigo/redis" +) + +// Graph represents a graph, which is a collection of nodes and edges. +type Graph struct { + Id string + Nodes map[string]*Node + Edges []*Edge + Conn redis.Conn + labels []string // List of node labels. + relationshipTypes []string // List of relation types. + properties []string // List of properties. +} + +// New creates a new graph. +func GraphNew(Id string, conn redis.Conn) Graph { + g := Graph { + Id: Id, + Nodes: make(map[string]*Node, 0), + Edges: make([]*Edge, 0), + Conn: conn, + labels: make([]string, 0), + relationshipTypes: make([]string, 0), + properties: make([]string, 0), + } + return g +} + +// AddNode adds a node to the graph. +func (g *Graph) AddNode(n *Node) { + if n.Alias == "" { + n.Alias = RandomString(10) + } + n.graph = g + g.Nodes[n.Alias] = n +} + +// AddEdge adds an edge to the graph. +func (g *Graph) AddEdge(e *Edge) error { + // Verify that the edge has source and destination + if e.Source == nil || e.Destination == nil { + return fmt.Errorf("Both source and destination nodes should be defined") + } + + // Verify that the edge's nodes have been previously added to the graph + if _, ok := g.Nodes[e.Source.Alias]; !ok { + return fmt.Errorf("Source node neeeds to be added to the graph first") + } + if _, ok := g.Nodes[e.Destination.Alias]; !ok { + return fmt.Errorf("Destination node neeeds to be added to the graph first") + } + + e.graph = g + g.Edges = append(g.Edges, e) + return nil +} + +// ExecutionPlan gets the execution plan for given query. +func (g *Graph) ExecutionPlan(q string) (string, error) { + return redis.String(g.Conn.Do("GRAPH.EXPLAIN", g.Id, q)) +} + +// Delete removes the graph. +func (g *Graph) Delete() error { + _, err := g.Conn.Do("GRAPH.DELETE", g.Id) + return err +} + +// Flush will create the graph and clear it +func (g *Graph) Flush() (*QueryResult, error) { + res, err := g.Commit() + if err == nil { + g.Nodes = make(map[string]*Node) + g.Edges = make([]*Edge, 0) + } + return res, err +} + +// Commit creates the entire graph, but will re-add nodes if called again. +func (g *Graph) Commit() (*QueryResult, error) { + items := make([]string, 0, len(g.Nodes)+len(g.Edges)) + for _, n := range g.Nodes { + items = append(items, n.Encode()) + } + for _, e := range g.Edges { + items = append(items, e.Encode()) + } + q := "CREATE " + strings.Join(items, ",") + return g.Query(q) +} + +// Query executes a query against the graph. +func (g *Graph) Query(q string) (*QueryResult, error) { + r, err := g.Conn.Do("GRAPH.QUERY", g.Id, q, "--compact") + if err != nil { + return nil, err + } + + qr := QueryResultNew(g, r) + return qr, nil +} + +// Merge pattern +func (g *Graph) Merge(p string) (*QueryResult, error) { + q := fmt.Sprintf("MERGE %s", p) + return g.Query(q) +} + +func (g *Graph) getLabel(lblIdx int) string { + if lblIdx >= len(g.labels) { + // Missing label + // refresh label mapping table. + g.labels = g.Labels() + + // Retry. + if lblIdx >= len(g.labels) { + // Error! + panic("Unknow label index.") + } + } + + return g.labels[lblIdx] +} + +func (g *Graph) getRelation(relIdx int) string { + if relIdx >= len(g.relationshipTypes) { + // Missing relation type + // refresh relation type mapping table. + g.relationshipTypes = g.RelationshipTypes() + + // Retry. + if relIdx >= len(g.relationshipTypes) { + // Error! + panic("Unknow relation type index.") + } + } + + return g.relationshipTypes[relIdx] +} + +func (g *Graph) getProperty(propIdx int) string { + if propIdx >= len(g.properties) { + // Missing property + // refresh property mapping table. + g.properties = g.PropertyKeys() + + // Retry. + if propIdx >= len(g.properties) { + // Error! + panic("Unknow property index.") + } + } + + return g.properties[propIdx] +} + +// Procedures + +// CallProcedure invokes procedure. +func (g *Graph) CallProcedure(procedure string, yield []string, args ...interface{}) (*QueryResult, error) { + q := fmt.Sprintf("CALL %s(", procedure) + + tmp := make([]string, 0, len(args)) + for arg := range args { + tmp = append(tmp, QuoteString(arg).(string)) + } + q += fmt.Sprintf("%s)", strings.Join(tmp, ",")) + + if yield != nil && len(yield) > 0 { + q += fmt.Sprintf(" YIELD %s", strings.Join(yield, ",")) + } + + return g.Query(q) +} + +// Labels, retrieves all node labels. +func (g *Graph) Labels() []string { + qr,_ := g.CallProcedure("db.labels", nil) + + l := make([]string, len(qr.results)) + + for idx, r := range(qr.results) { + l[idx] = r[0].(string) + } + return l +} + +// RelationshipTypes, retrieves all edge relationship types. +func (g *Graph) RelationshipTypes() []string { + qr,_ := g.CallProcedure("db.relationshipTypes", nil) + + rt := make([]string, len(qr.results)) + + for idx, r := range(qr.results) { + rt[idx] = r[0].(string) + } + return rt +} + +// PropertyKeys, retrieves all properties names. +func (g *Graph) PropertyKeys() []string { + qr,_ := g.CallProcedure("db.propertyKeys", nil) + + p := make([]string, len(qr.results)) + + for idx, r := range(qr.results) { + p[idx] = r[0].(string) + } + return p +} diff --git a/node.go b/node.go new file mode 100644 index 0000000..a0d376c --- /dev/null +++ b/node.go @@ -0,0 +1,81 @@ +package redisgraph + +import ( + "fmt" + "strings" +) + +// Node represents a node within a graph. +type Node struct { + ID uint64 + Label string + Alias string + Properties map[string]interface{} + graph *Graph +} + +func NodeNew(id uint64, label string, alias string, properties map[string]interface{}) *Node { + + p := properties + if p == nil { + p = make(map[string]interface{}) + } + + return &Node{ + ID: id, + Label: label, + Alias: alias, + Properties: p, + graph: nil, + } +} + +func (n *Node) SetProperty(key string, value interface{}) { + n.Properties[key] = value +} + +func (n Node) GetProperty(key string) interface{} { + v,_ := n.Properties[key] + return v +} + +func (n Node) String() string { + if len(n.Properties) == 0 { + return "{}" + } + + p := make([]string, 0, len(n.Properties)) + for k, v := range n.Properties { + p = append(p, fmt.Sprintf("%s:%v", k, QuoteString(v))) + } + + s := fmt.Sprintf("{%s}", strings.Join(p, ",")) + return s +} + +// String makes Node satisfy the Stringer interface. +func (n Node) Encode() string { + s := []string{"("} + + if n.Alias != "" { + s = append(s, n.Alias) + } + + if n.Label != "" { + s = append(s, ":", n.Label) + } + + if len(n.Properties) > 0 { + p := make([]string, 0, len(n.Properties)) + for k, v := range n.Properties { + p = append(p, fmt.Sprintf("%s:%v", k, QuoteString(v))) + } + + s = append(s, "{") + s = append(s, strings.Join(p, ",")) + s = append(s, "}") + } + + s = append(s, ")") + return strings.Join(s, "") +} diff --git a/query_result.go b/query_result.go new file mode 100644 index 0000000..e580e75 --- /dev/null +++ b/query_result.go @@ -0,0 +1,244 @@ +package redisgraph + +import ( + "fmt" + "os" + "strings" + "strconv" + + "github.com/gomodule/redigo/redis" + "github.com/olekukonko/tablewriter" +) + +type ResultSetColumnTypes int +const ( + COLUMN_UNKNOWN ResultSetColumnTypes = iota + COLUMN_SCALAR + COLUMN_NODE + COLUMN_RELATION +) + +type ResultSetScalarTypes int +const ( + PROPERTY_UNKNOWN ResultSetScalarTypes = iota + PROPERTY_NULL + PROPERTY_STRING + PROPERTY_INTEGER + PROPERTY_BOOLEAN + PROPERTY_DOUBLE +) + +type QueryResultHeader struct { + column_names []string + column_types []ResultSetColumnTypes +} + +// QueryResult represents the results of a query. +type QueryResult struct { + results [][]interface{} + statistics map[string]float64 + header QueryResultHeader + graph *Graph +} + +func QueryResultNew(g *Graph, response interface{}) *QueryResult { + qr := &QueryResult { + results: nil, + statistics: nil, + header: QueryResultHeader { + column_names: make([]string, 0), + column_types: make([]ResultSetColumnTypes, 0), + }, + graph: g, + } + + r, _ := redis.Values(response, nil) + if len(r) == 1 { + qr.parseStatistics(r[0]) + } else{ + qr.parseResults(r) + qr.parseStatistics(r[2]) + } + + + return qr +} + +func (qr *QueryResult) Empty() bool { + return len(qr.results) == 0 +} + +func (qr *QueryResult) parseResults(raw_result_set []interface{}) { + header := raw_result_set[0] + qr.parseHeader(header) + qr.parseRecords(raw_result_set) +} + +func (qr *QueryResult) parseStatistics(raw_statistics interface{}) { + statistics,_ := redis.Strings(raw_statistics, nil) + qr.statistics = make(map[string]float64) + + for _,rs := range(statistics) { + v := strings.Split(rs, ": ") + f,_ := strconv.ParseFloat(strings.Split(v[1], " ")[0], 64) + qr.statistics[v[0]] = f + } +} + +func (qr *QueryResult) parseHeader(raw_header interface{}) { + header, _ := redis.Values(raw_header, nil) + + for _,col := range(header) { + c, _ := redis.Values(col, nil) + ct, _ := redis.Int(c[0], nil) + cn, _ := redis.String(c[1], nil) + + qr.header.column_types = append(qr.header.column_types, ResultSetColumnTypes(ct)) + qr.header.column_names = append(qr.header.column_names, cn) + } +} + +func (qr *QueryResult) parseRecords(raw_result_set []interface{}) { + records, _ := redis.Values(raw_result_set[1], nil) + + qr.results = make([][]interface{}, len(records)) + + for i, r := range(records) { + cells, _ := redis.Values(r, nil) + record := make([]interface{}, len(cells)) + + for idx, c := range(cells) { + t := qr.header.column_types[idx] + switch(t) { + case COLUMN_SCALAR: + s, _ := redis.Values(c, nil) + record[idx] = qr.parseScalar(s) + break + case COLUMN_NODE: + record[idx] = qr.parseNode(c) + break + case COLUMN_RELATION: + record[idx] = qr.parseEdge(c) + break + default: + panic("Unknown column type.") + } + } + qr.results[i] = record + } +} + +func (qr *QueryResult) parseProperties(props []interface{}) map[string]interface{} { + // [[name, value type, value] X N] + properties := make (map[string]interface{}) + for _,prop := range (props) { + p, _ := redis.Values(prop, nil) + idx, _ := redis.Int(p[0], nil) + prop_name := qr.graph.getProperty(idx) + prop_value := qr.parseScalar(p[1:]) + properties[prop_name] = prop_value + } + + return properties +} + +func (qr *QueryResult) parseNode(cell interface{}) *Node { + // Node ID (integer), + // [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]) + } + + rawProps, _ := redis.Values(c[2], nil) + properties := qr.parseProperties(rawProps) + + return NodeNew(id, label, "", properties) +} + +func (qr *QueryResult) parseEdge(cell interface{}) *Edge { + // Edge ID (integer), + // reltype string offset (integer), + // src node ID offset (integer), + // dest node ID offset (integer), + // [[name, value, value type] X N] + + c,_ := redis.Values(cell, nil) + id,_ := redis.Uint64(c[0], nil) + r,_ := redis.Int(c[1], nil) + relation := qr.graph.getRelation(r) + + src_node_id, _ := redis.Uint64(c[2], nil) + dest_node_id, _ := redis.Uint64(c[3], nil) + rawProps,_ := redis.Values(c[4], nil) + properties := qr.parseProperties(rawProps) + e := EdgeNew(id, relation, nil, nil, properties) + + e.srcNodeID = src_node_id + e.destNodeID = dest_node_id + return e +} + +func (qr *QueryResult) parseScalar(cell []interface{}) interface{} { + t,_ := redis.Int(cell[0], nil) + v := cell[1] + var s interface{} + switch(ResultSetScalarTypes(t)) { + case PROPERTY_NULL: + return nil + + case PROPERTY_STRING: + s,_ = redis.String(v, nil) + + case PROPERTY_INTEGER: + s,_ = redis.Int(v, nil) + + case PROPERTY_BOOLEAN: + s,_ = redis.Bool(v, nil) + + case PROPERTY_DOUBLE: + s,_ = redis.Float64(v, nil) + + case PROPERTY_UNKNOWN: + panic("Unknown scalar type\n") + } + + return s +} + +// PrettyPrint prints the QueryResult to stdout, pretty-like. +func (qr *QueryResult) PrettyPrint() { + if qr.Empty() { + return + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetAutoFormatHeaders(false) + table.SetHeader(qr.header.column_names) + + if len(qr.results) > 0 { + // Convert to [][]string. + results := make([][]string, len(qr.results)) + for i, record := range(qr.results) { + results[i] = make([]string, len(record)) + for j, elem := range(record) { + results[i][j] = fmt.Sprint(elem) + } + } + table.AppendBulk(results) + } else { + table.Append([]string{"No data returned."}) + } + table.Render() + + for _, stat := range qr.statistics { + fmt.Fprintf(os.Stdout, "\n%s", stat) + } + + fmt.Fprintf(os.Stdout, "\n") +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..08dbe8d --- /dev/null +++ b/utils.go @@ -0,0 +1,48 @@ +package redisgraph + +import ( + "crypto/rand" +) + +func QuoteString(i interface{}) interface{} { + switch x := i.(type) { + case string: + if len(x) == 0 { + return "\"\"" + } + if x[0] != '"' { + x = "\"" + x + } + if x[len(x)-1] != '"' { + x += "\"" + } + return x + default: + return i + } +} + +// https://medium.com/@kpbird/golang-generate-fixed-size-random-string-dd6dbd5e63c0 +func RandomString(n int) string { + const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + output := make([]byte, n) + // We will take n bytes, one byte for each character of output. + randomness := make([]byte, n) + // read all random + _, err := rand.Read(randomness) + if err != nil { + panic(err) + } + l := len(letterBytes) + // fill output + for pos := range output { + // get random item + random := uint8(randomness[pos]) + // random % 64 + randomPos := random % uint8(l) + // put into output + output[pos] = letterBytes[randomPos] + } + return string(output) +} \ No newline at end of file From 9fb9b98bfb4c09ea46558fdde3c048a16ad353d4 Mon Sep 17 00:00:00 2001 From: swilly22 Date: Fri, 3 May 2019 11:25:13 +0300 Subject: [PATCH 2/2] protect graph internal state update --- client_test.go | 115 ++++++++++++++++++++++++++++++++++++------------ edge.go | 19 ++++++-- graph.go | 51 +++++++++++++-------- node.go | 5 +-- query_result.go | 56 +++++++++++++++++++++-- 5 files changed, 192 insertions(+), 54 deletions(-) diff --git a/client_test.go b/client_test.go index a2cad33..5c2dade 100644 --- a/client_test.go +++ b/client_test.go @@ -2,54 +2,115 @@ package redisgraph import ( "testing" - + "os" + "time" + "github.com/stretchr/testify/assert" "github.com/gomodule/redigo/redis" ) -func TestGraphCreation(t *testing.T) { - // Setup. - conn, _ := redis.Dial("tcp", "0.0.0.0:6379") - defer conn.Close() +var graph Graph +func createGraph() { + conn, _ := redis.Dial("tcp", "0.0.0.0:6379") conn.Do("FLUSHALL") - rg := GraphNew("social", conn) + graph = GraphNew("social", conn) // Create 2 nodes connect via a single edge. - japan := NodeNew(0, "country", "j", nil) - john := NodeNew(0, "person", "p", nil) - edge := EdgeNew(0, "visited", john, japan, nil) + japan := NodeNew("Country", "j", nil) + john := NodeNew("Person", "p", nil) + edge := EdgeNew("Visited", john, japan, nil) // Set node properties. john.SetProperty("name", "John Doe") john.SetProperty("age", 33) john.SetProperty("gender", "male") john.SetProperty("status", "single") + + japan.SetProperty("name", "Japan") + japan.SetProperty("population", 126800000) + + edge.SetProperty("year", 2017) // Introduce entities to graph. - rg.AddNode(john) - rg.AddNode(japan) - rg.AddEdge(edge) + graph.AddNode(john) + graph.AddNode(japan) + graph.AddEdge(edge) // Flush graph to DB. - resp, err := rg.Commit() + _, err := graph.Commit() + if err != nil { + panic(err) + } +} + +func setup() { + createGraph() +} + +func shutdown() { + graph.Conn.Close() +} + +func TestMain(m *testing.M) { + setup() + code := m.Run() + shutdown() + os.Exit(code) +} + +func TestMatchQuery(t *testing.T) { + q := "MATCH (s)-[e]->(d) RETURN s,e,d" + res, err := graph.Query(q) if err != nil { t.Error(err) } + + assert.Equal(t, len(res.results), 1, "expecting 1 result record") + + s, ok := (res.results[0][0]).(*Node) + assert.True(t, ok, "First column should contain nodes.") + e, ok := (res.results[0][1]).(*Edge) + assert.True(t, ok, "Second column should contain edges.") + d, ok := (res.results[0][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, 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, len(s.Properties), 4, "Person node should have 4 properties") + + assert.Equal(t, s.GetProperty("name"), "John Doe", "Unexpected property value.") + assert.Equal(t, s.GetProperty("age"), 33, "Unexpected property value.") + assert.Equal(t, s.GetProperty("gender"), "male", "Unexpected property value.") + assert.Equal(t, s.GetProperty("status"), "single", "Unexpected property value.") + + assert.Equal(t, e.GetProperty("year"), 2017, "Unexpected property value.") + + assert.Equal(t, d.GetProperty("name"), "Japan", "Unexpected property value.") + assert.Equal(t, d.GetProperty("population"), 126800000, "Unexpected property value.") +} - // Validate response. - if(resp.results != nil) { - t.FailNow() +func TestCreateQuery(t *testing.T) { + q := "CREATE (w:WorkPlace {name:'RedisLabs'})" + res, err := graph.Query(q) + if err != nil { + t.Error(err) } - if(resp.statistics["Labels added"] != 2) { - t.FailNow() + + assert.True(t, res.Empty(), "Expecting empty result-set") + + // Validate statistics. + assert.Equal(t, res.NodesCreated(), 1, "Expecting a single node to be created.") + assert.Equal(t, res.PropertiesSet(), 1, "Expecting a songle property to be added.") + + q = "MATCH (w:WorkPlace) RETURN w" + res, err = graph.Query(q) + if err != nil { + t.Error(err) } - if(resp.statistics["Nodes created"] != 2) { - t.FailNow() - } - if(resp.statistics["Properties set"] != 4) { - t.FailNow() - } - if(resp.statistics["Relationships created"] != 1) { - t.FailNow() - } + + assert.False(t, res.Empty(), "Expecting resultset to include a single node.") + w := (res.results[0][0]).(*Node) + assert.Equal(t, w.Label, "WorkPlace", "Unexpected node label.") } diff --git a/edge.go b/edge.go index 68edd17..8b6d93a 100644 --- a/edge.go +++ b/edge.go @@ -17,17 +17,30 @@ type Edge struct { graph *Graph } -func EdgeNew(id uint64, relation string, srcNode *Node, destNode *Node, properties map[string]interface{}) *Edge { +func EdgeNew(relation string, srcNode *Node, destNode *Node, properties map[string]interface{}) *Edge { + p := properties + if p == nil { + p = make(map[string]interface{}) + } + return &Edge{ - ID:id, Relation: relation, Source: srcNode, Destination: destNode, - Properties: properties, + Properties: p, graph: nil, } } +func (e *Edge) SetProperty(key string, value interface{}) { + e.Properties[key] = value +} + +func (e *Edge) GetProperty(key string) interface{} { + v,_ := e.Properties[key] + return v +} + func (e Edge) SourceNodeID() uint64 { if(e.Source != nil) { return e.Source.ID diff --git a/graph.go b/graph.go index d65371e..f4f40a2 100644 --- a/graph.go +++ b/graph.go @@ -3,6 +3,7 @@ package redisgraph import ( "fmt" "strings" + "sync" "github.com/gomodule/redigo/redis" ) @@ -16,6 +17,7 @@ type Graph struct { labels []string // List of node labels. relationshipTypes []string // List of relation types. properties []string // List of properties. + mutex sync.Mutex // Lock, used for updating internal state. } // New creates a new graph. @@ -114,15 +116,19 @@ func (g *Graph) Merge(p string) (*QueryResult, error) { func (g *Graph) getLabel(lblIdx int) string { if lblIdx >= len(g.labels) { - // Missing label - // refresh label mapping table. - g.labels = g.Labels() + // Missing label, refresh label mapping table. + g.mutex.Lock() - // Retry. + // Recheck now that we've got the lock. if lblIdx >= len(g.labels) { - // Error! - panic("Unknow label index.") + g.labels = g.Labels() + // Retry. + if lblIdx >= len(g.labels) { + // Error! + panic("Unknow label index.") + } } + g.mutex.Unlock() } return g.labels[lblIdx] @@ -130,15 +136,19 @@ func (g *Graph) getLabel(lblIdx int) string { func (g *Graph) getRelation(relIdx int) string { if relIdx >= len(g.relationshipTypes) { - // Missing relation type - // refresh relation type mapping table. - g.relationshipTypes = g.RelationshipTypes() + // Missing relation type, refresh relation type mapping table. + g.mutex.Lock() - // Retry. + // Recheck now that we've got the lock. if relIdx >= len(g.relationshipTypes) { - // Error! - panic("Unknow relation type index.") + g.relationshipTypes = g.RelationshipTypes() + // Retry. + if relIdx >= len(g.relationshipTypes) { + // Error! + panic("Unknow relation type index.") + } } + g.mutex.Unlock() } return g.relationshipTypes[relIdx] @@ -146,15 +156,20 @@ func (g *Graph) getRelation(relIdx int) string { func (g *Graph) getProperty(propIdx int) string { if propIdx >= len(g.properties) { - // Missing property - // refresh property mapping table. - g.properties = g.PropertyKeys() + // Missing property, refresh property mapping table. + g.mutex.Lock() - // Retry. + // Recheck now that we've got the lock. if propIdx >= len(g.properties) { - // Error! - panic("Unknow property index.") + g.properties = g.PropertyKeys() + + // Retry. + if propIdx >= len(g.properties) { + // Error! + panic("Unknow property index.") + } } + g.mutex.Unlock() } return g.properties[propIdx] diff --git a/node.go b/node.go index a0d376c..ef084cd 100644 --- a/node.go +++ b/node.go @@ -14,15 +14,14 @@ type Node struct { graph *Graph } -func NodeNew(id uint64, label string, alias string, properties map[string]interface{}) *Node { +func NodeNew(label string, alias string, properties map[string]interface{}) *Node { p := properties if p == nil { p = make(map[string]interface{}) } - return &Node{ - ID: id, + return &Node{ Label: label, Alias: alias, Properties: p, diff --git a/query_result.go b/query_result.go index e580e75..88369a1 100644 --- a/query_result.go +++ b/query_result.go @@ -10,6 +10,16 @@ import ( "github.com/olekukonko/tablewriter" ) +const ( + LABELS_ADDED string = "Labels added" + NODES_CREATED string = "Nodes created" + NODES_DELETED string = "Nodes deleted" + RELATIONSHIPS_DELETED string = "Relationships deleted" + PROPERTIES_SET string = "Properties set" + RELATIONSHIPS_CREATED string = "Relationships created" + INTERNAL_EXECUTION_TIME string = "internal execution time" +) + type ResultSetColumnTypes int const ( COLUMN_UNKNOWN ResultSetColumnTypes = iota @@ -158,7 +168,9 @@ func (qr *QueryResult) parseNode(cell interface{}) *Node { rawProps, _ := redis.Values(c[2], nil) properties := qr.parseProperties(rawProps) - return NodeNew(id, label, "", properties) + n := NodeNew(label, "", properties) + n.ID = id + return n } func (qr *QueryResult) parseEdge(cell interface{}) *Edge { @@ -177,8 +189,9 @@ func (qr *QueryResult) parseEdge(cell interface{}) *Edge { dest_node_id, _ := redis.Uint64(c[3], nil) rawProps,_ := redis.Values(c[4], nil) properties := qr.parseProperties(rawProps) - e := EdgeNew(id, relation, nil, nil, properties) - + e := EdgeNew(relation, nil, nil, properties) + + e.ID = id e.srcNodeID = src_node_id e.destNodeID = dest_node_id return e @@ -242,3 +255,40 @@ func (qr *QueryResult) PrettyPrint() { fmt.Fprintf(os.Stdout, "\n") } + + +func (qr *QueryResult) getStat(stat string) int { + if val, ok := qr.statistics[stat]; ok { + return int(val) + } else { + return 0 + } +} + +func (qr *QueryResult) LabelsAdded() int { + return qr.getStat(LABELS_ADDED) +} + +func (qr *QueryResult) NodesCreated() int { + return qr.getStat(NODES_CREATED) +} + +func (qr *QueryResult) NodesDeleted() int { + return qr.getStat(NODES_DELETED) +} + +func (qr *QueryResult) PropertiesSet() int { + return qr.getStat(PROPERTIES_SET) +} + +func (qr *QueryResult) RelationshipsCreated() int { + return qr.getStat(RELATIONSHIPS_CREATED) +} + +func (qr *QueryResult) RelationshipsDeleted() int { + return qr.getStat(RELATIONSHIPS_DELETED) +} + +func (qr *QueryResult) RunTime() int { + return qr.getStat(INTERNAL_EXECUTION_TIME) +} \ No newline at end of file