// EnsureReverseEdges checks if gs contains reverse edges. If it doesn't, it // will scan gs for all forward edges, adding a reverse for each back into the // GraphStore. This is necessary for a GraphStoreService to work properly. func EnsureReverseEdges(ctx context.Context, gs graphstore.Service) error { var edge *spb.Entry if err := gs.Scan(ctx, &spb.ScanRequest{}, func(e *spb.Entry) error { if graphstore.IsEdge(e) { edge = e return io.EOF } return nil }); err != nil { return err } if edge == nil { log.Println("No edges found in GraphStore") return nil } else if schema.EdgeDirection(edge.EdgeKind) == schema.Reverse { return nil } var foundReverse bool if err := gs.Read(ctx, &spb.ReadRequest{ Source: edge.Target, EdgeKind: schema.MirrorEdge(edge.EdgeKind), }, func(entry *spb.Entry) error { foundReverse = true return nil }); err != nil { return fmt.Errorf("error checking for reverse edge: %v", err) } if foundReverse { return nil } return addReverseEdges(ctx, gs) }
func addReverseEdges(ctx context.Context, gs graphstore.Service) error { log.Println("Adding reverse edges") var ( totalEntries int addedEdges int ) startTime := time.Now() err := gs.Scan(ctx, new(spb.ScanRequest), func(entry *spb.Entry) error { kind := entry.EdgeKind if kind != "" && schema.EdgeDirection(kind) == schema.Forward { if err := gs.Write(ctx, &spb.WriteRequest{ Source: entry.Target, Update: []*spb.WriteRequest_Update{{ Target: entry.Source, EdgeKind: schema.MirrorEdge(kind), FactName: entry.FactName, FactValue: entry.FactValue, }}, }); err != nil { return fmt.Errorf("Failed to write reverse edge: %v", err) } addedEdges++ } totalEntries++ return nil }) log.Printf("Wrote %d reverse edges to GraphStore (%d total entries): %v", addedEdges, totalEntries, time.Since(startTime)) return err }
// CrossReference returns a (Referent, TargetAnchor) *ipb.CrossReference // equivalent to the given decoration. The decoration's anchor is expanded // given its parent file and associated Normalizer. func CrossReference(file *srvpb.File, norm *xrefs.Normalizer, d *srvpb.FileDecorations_Decoration, tgt *srvpb.Node) (*ipb.CrossReference, error) { if file == nil || norm == nil { return nil, errors.New("missing decoration's parent file") } ea, err := ExpandAnchor(d.Anchor, file, norm, schema.MirrorEdge(d.Kind)) if err != nil { return nil, fmt.Errorf("error expanding anchor {%+v}: %v", d.Anchor, err) } // Throw away most of the referent's facts. They are not needed. var facts []*cpb.Fact if tgt != nil { for _, fact := range tgt.Fact { if fact.Name == schema.CompleteFact { facts = append(facts, fact) } } } return &ipb.CrossReference{ Referent: &srvpb.Node{ Ticket: d.Target, Fact: facts, }, TargetAnchor: ea, }, nil }
// ReverseSinglePath returns the reverse p, assuming it is a single-edge Path. func ReverseSinglePath(p *ipb.Path) *ipb.Path { return &ipb.Path{ Pivot: p.Edges[0].Target, Edges: []*ipb.Path_Edge{{ Kind: schema.MirrorEdge(p.Edges[0].Kind), Ordinal: p.Edges[0].Ordinal, Target: p.Pivot, }}, } }
// ToString returns a human-readable string representation of p. func ToString(p *ipb.Path) string { s := "**" + p.Pivot.NodeKind + "**" for _, e := range p.Edges { if schema.EdgeDirection(e.Kind) == schema.Forward { s += fmt.Sprintf(" -[%s]> %s", e.Kind, e.Target.NodeKind) } else { s += fmt.Sprintf(" <[%s]- %s", schema.MirrorEdge(e.Kind), e.Target.NodeKind) } } return s }
// expandEdgeKind prefixes unrooted (not starting with "/") edge kinds with the // standard Kythe edge prefix ("/kythe/edge/"). func expandEdgeKind(kind string) string { ck := schema.Canonicalize(kind) if strings.HasPrefix(ck, "/") { return kind } expansion := schema.EdgePrefix + ck if schema.EdgeDirection(kind) == schema.Reverse { return schema.MirrorEdge(expansion) } return expansion }
// CrossReference returns a (Referent, TargetAnchor) *srvpb.CrossReference // equivalent to the given decoration. The decoration's anchor is expanded // given its parent file and associated Normalizer. func CrossReference(file *srvpb.File, norm *xrefs.Normalizer, d *srvpb.FileDecorations_Decoration) (*srvpb.CrossReference, error) { if file == nil || norm == nil { return nil, errors.New("missing decoration's parent file") } ea, err := expandAnchor(d.Anchor, file, norm, schema.MirrorEdge(d.Kind)) if err != nil { return nil, fmt.Errorf("error expanding anchor {%+v}: %v", d.Anchor, err) } return &srvpb.CrossReference{ // Throw away the referent's facts. They are not needed. Referent: &srvpb.Node{Ticket: d.Target.Ticket}, TargetAnchor: ea, }, nil }
func writeCompletedEdges(ctx context.Context, edges disksort.Interface, e *srvpb.Edge) error { if err := edges.Add(&srvpb.Edge{ Source: &srvpb.Node{Ticket: e.Source.Ticket}, Kind: e.Kind, Target: e.Target, }); err != nil { return fmt.Errorf("error writing complete edge: %v", err) } if err := edges.Add(&srvpb.Edge{ Source: &srvpb.Node{Ticket: e.Target.Ticket}, Kind: schema.MirrorEdge(e.Kind), Target: assemble.FilterTextFacts(e.Source), }); err != nil { return fmt.Errorf("error writing complete edge mirror: %v", err) } return nil }
func writeWithReverses(ctx context.Context, tbl *table.KVProto, src, kind string, targets []string) error { if err := tbl.Put(ctx, []byte(src+tempTableKeySep+kind+tempTableKeySep), &srvpb.EdgeSet_Group{ Kind: kind, TargetTicket: targets, }); err != nil { return fmt.Errorf("error writing edges group: %v", err) } revGroup := &srvpb.EdgeSet_Group{ Kind: schema.MirrorEdge(kind), TargetTicket: []string{src}, } for _, tgt := range targets { if err := tbl.Put(ctx, []byte(tgt+tempTableKeySep+revGroup.Kind+tempTableKeySep+src), revGroup); err != nil { return fmt.Errorf("error writing rev edges group: %v", err) } } return nil }
func writeWithReverses(ctx context.Context, gs graphstore.Service, req *spb.WriteRequest) error { if err := gs.Write(ctx, req); err != nil { return fmt.Errorf("error writing edges: %v", err) } for _, u := range req.Update { if err := gs.Write(ctx, &spb.WriteRequest{ Source: u.Target, Update: []*spb.WriteRequest_Update{{ Target: req.Source, EdgeKind: schema.MirrorEdge(u.EdgeKind), FactName: u.FactName, FactValue: u.FactValue, }}, }); err != nil { return fmt.Errorf("error writing rev edge: %v", err) } } return nil }
// PartialReverseEdges returns the set of partial reverse edges from the given source. Each // reversed Edge has its Target fully populated and its Source will have no facts. To ensure every // node has at least 1 Edge, the first Edge will be a self-edge without a Kind or Target. To reduce // the size of edge sets, each Target will have any text facts filtered (see FilterTextFacts). func PartialReverseEdges(src *Source) []*srvpb.Edge { node := src.Node() edges := []*srvpb.Edge{{ Source: node, // self-edge to ensure every node has at least 1 edge }} targetNode := FilterTextFacts(node) for kind, targets := range src.Edges { rev := schema.MirrorEdge(kind) for _, target := range targets { edges = append(edges, &srvpb.Edge{ Source: &srvpb.Node{Ticket: target}, Kind: rev, Target: targetNode, }) } } return edges }
// FromSource creates a set of *ipb.Paths for each of its edges as well as a // single *ipb.Path with only its pivot set to be the source node. func FromSource(src *ipb.Source) []*ipb.Path { var paths []*ipb.Path n := specialize(assemble.Node(src)) paths = append(paths, &ipb.Path{Pivot: n}) for kind, group := range src.EdgeGroups { if schema.EdgeDirection(kind) != schema.Forward { continue } for _, tgt := range group.Edges { paths = append(paths, &ipb.Path{ Pivot: &ipb.Path_Node{ Ticket: tgt.Ticket, }, Edges: []*ipb.Path_Edge{{ Kind: schema.MirrorEdge(kind), Ordinal: int32(tgt.Ordinal), Target: n, }}, }) } } return paths }
} `json:"span"` Kind string `json:"kind"` Node struct { Ticket string `json:"ticket"` Names []string `json:"names,omitempty"` Kind string `json:"kind,omitempty"` Subkind string `json:"subkind,omitempty"` Typed string `json:"typed,omitempty"` Definitions []*definition `json:"definitions,omitempty"` } `json:"node"` } var ( definedAtEdge = schema.MirrorEdge(schema.DefinesEdge) definedBindingAtEdge = schema.MirrorEdge(schema.DefinesBindingEdge) ) func main() { flag.Parse() if flag.NArg() > 0 { flagutil.UsageErrorf("unknown non-flag argument(s): %v", flag.Args()) } else if *offset < 0 && (*lineNumber < 0 || *columnOffset < 0) { flagutil.UsageError("non-negative --offset (or --line and --column) required") } else if *signature == "" && *path == "" { flagutil.UsageError("must provide at least --path or --signature") } defer (*apiFlag).Close() xs, idx = *apiFlag, *apiFlag
if err := gs.Write(ctx, &spb.WriteRequest{ Source: u.Target, Update: []*spb.WriteRequest_Update{{ Target: req.Source, EdgeKind: schema.MirrorEdge(u.EdgeKind), FactName: u.FactName, FactValue: u.FactValue, }}, }); err != nil { return fmt.Errorf("error writing rev edge: %v", err) } } return nil } var revChildOfEdgeKind = schema.MirrorEdge(schema.ChildOfEdge) func writeDecorations(ctx context.Context, t table.Proto, es xrefs.NodesEdgesService, files []string) error { log.Println("Writing Decorations") edges := make(chan *xpb.EdgesReply) var eErr error go func() { eErr = readEdges(ctx, es, files, edges, decorationFilters, []string{revChildOfEdgeKind}) close(edges) }() for e := range edges { decor := &srvpb.FileDecorations{} if len(e.EdgeSet) == 0 {
testAnchorVName = sig("testAnchor") testAnchorTargetVName = sig("someSemanticNode") testNodes = []*node{ {sig("orphanedNode"), facts(schema.NodeKindFact, "orphan"), nil}, {testFileVName, facts( schema.NodeKindFact, schema.FileKind, schema.TextFact, testFileContent, schema.TextEncodingFact, testFileEncoding), map[string][]*spb.VName{ revChildOfEdgeKind: {testAnchorVName}, }}, {sig("sig2"), facts(schema.NodeKindFact, "test"), map[string][]*spb.VName{ "someEdgeKind": {sig("signature")}, }}, {sig("signature"), facts(schema.NodeKindFact, "test"), map[string][]*spb.VName{ schema.MirrorEdge("someEdgeKind"): {sig("sig2")}, }}, {testAnchorVName, facts( schema.AnchorEndFact, "4", schema.AnchorStartFact, "1", schema.NodeKindFact, schema.AnchorKind, ), map[string][]*spb.VName{ schema.ChildOfEdge: {testFileVName}, schema.RefEdge: {testAnchorTargetVName}, }}, {testAnchorTargetVName, facts(schema.NodeKindFact, "record"), map[string][]*spb.VName{ schema.MirrorEdge(schema.RefEdge): {testAnchorVName}, }}, } testEntries = nodesToEntries(testNodes) )
Ticket: allRelatedNodes.Slice(), Filter: req.Filter, }) if err != nil { return nil, fmt.Errorf("error retrieving related nodes: %v", err) } for _, n := range nReply.Node { reply.Nodes[n.Ticket] = n } } return reply, nil } var ( revDefinesEdge = schema.MirrorEdge(schema.DefinesEdge) revDefinesBindingEdge = schema.MirrorEdge(schema.DefinesBindingEdge) revRefEdge = schema.MirrorEdge(schema.RefEdge) revDocumentsEdge = schema.MirrorEdge(schema.DocumentsEdge) ) func isDefKind(requestedKind xpb.CrossReferencesRequest_DefinitionKind, edgeKind string) bool { switch requestedKind { case xpb.CrossReferencesRequest_NO_DEFINITIONS: return false case xpb.CrossReferencesRequest_FULL_DEFINITIONS: return edgeKind == revDefinesEdge case xpb.CrossReferencesRequest_BINDING_DEFINITIONS: return edgeKind == revDefinesBindingEdge
End int `json:"end"` Text string `json:"text,omitempty"` } `json:"span"` Kind string `json:"kind"` Node struct { Ticket string `json:"ticket"` Names []string `json:"names,omitempty"` Kind string `json:"kind,omitempty"` Subkind string `json:"subkind,omitempty"` Definitions []*definition `json:"definitions,omitempty"` } `json:"node"` } var definedAtEdge = schema.MirrorEdge(schema.DefinesEdge) func main() { flag.Parse() if *offset < 0 { flagutil.UsageError("non-negative --offset required") } else if *signature == "" && *path == "" { flagutil.UsageError("must provide at least --path or --signature") } if strings.HasPrefix(*remoteAPI, "http://") || strings.HasPrefix(*remoteAPI, "https://") { xs = xrefs.WebClient(*remoteAPI) idx = search.WebClient(*remoteAPI) } else { conn, err := grpc.Dial(*remoteAPI) if err != nil {
func TestSlowDocumentation(t *testing.T) { db := []struct { ticket, kind, documented, defines, completes, completed, childof, typed, text, format string params, definitionText []string }{ {ticket: "kythe://test#a", kind: "etc", documented: "kythe://test#adoc", format: "asig"}, {ticket: "kythe://test#adoc", kind: "doc", text: "atext"}, {ticket: "kythe://test#fdoc", kind: "doc", text: "ftext"}, {ticket: "kythe://test#fdecl", kind: "function", documented: "kythe://test#fdoc", format: "fsig"}, {ticket: "kythe://test#fdefn", kind: "function", completed: "kythe://test#fbind", format: "fsig"}, {ticket: "kythe://test#fbind", kind: "anchor", defines: "kythe://test#fdefn", completes: "kythe://test#fdecl"}, {ticket: "kythe://test#l", kind: "etc", documented: "kythe://test#ldoc"}, {ticket: "kythe://test#ldoc", kind: "doc", text: "ltext", params: []string{"kythe://test#l1", "kythe://test#l2"}}, {ticket: "kythe://test#l1", kind: "etc", definitionText: []string{"deftext1"}}, {ticket: "kythe://test#l2", kind: "etc", definitionText: []string{"deftext2"}}, {ticket: "kythe://test#l", kind: "etc", documented: "kythe://test#ldoc", format: "lsig"}, } mkPr := func(text string, linkTicket ...string) *xpb.Printable { links := make([]*xpb.Link, len(linkTicket)) for i, link := range linkTicket { links[i] = &xpb.Link{Definition: []*xpb.Anchor{{Text: link}}} } return &xpb.Printable{RawText: text, Link: links} } tests := []struct { ticket string reply *xpb.DocumentationReply_Document }{ {ticket: "kythe://test#a", reply: &xpb.DocumentationReply_Document{Signature: mkPr("asig"), Kind: "etc", Text: mkPr("atext")}}, {ticket: "kythe://test#fdecl", reply: &xpb.DocumentationReply_Document{Signature: mkPr("fsig"), Kind: "function", Text: mkPr("ftext")}}, {ticket: "kythe://test#fdefn", reply: &xpb.DocumentationReply_Document{Signature: mkPr("fsig"), Kind: "function", Text: mkPr("ftext")}}, {ticket: "kythe://test#l", reply: &xpb.DocumentationReply_Document{Signature: mkPr("lsig"), Kind: "etc", Text: mkPr("ltext", "deftext1", "deftext2")}}, } nodes := make(map[string]*xpb.NodeInfo) edges := make(map[string]*xpb.EdgeSet) xrefs := make(map[string]*xpb.CrossReferencesReply_CrossReferenceSet) for _, node := range db { nodes[node.ticket] = &xpb.NodeInfo{ Facts: map[string][]byte{ schema.NodeKindFact: []byte(node.kind), schema.TextFact: []byte(node.text), schema.FormatFact: []byte(node.format), }, } if node.definitionText != nil { set := &xpb.CrossReferencesReply_CrossReferenceSet{Ticket: node.ticket} for _, text := range node.definitionText { set.Definition = append(set.Definition, &xpb.CrossReferencesReply_RelatedAnchor{Anchor: &xpb.Anchor{Text: text}}) } xrefs[node.ticket] = set } set := &xpb.EdgeSet{Groups: make(map[string]*xpb.EdgeSet_Group)} if node.typed != "" { set.Groups[schema.TypedEdge] = &xpb.EdgeSet_Group{ Edge: []*xpb.EdgeSet_Group_Edge{&xpb.EdgeSet_Group_Edge{TargetTicket: node.typed}}, } } if node.childof != "" { set.Groups[schema.ChildOfEdge] = &xpb.EdgeSet_Group{ Edge: []*xpb.EdgeSet_Group_Edge{&xpb.EdgeSet_Group_Edge{TargetTicket: node.childof}}, } } if node.documented != "" { set.Groups[schema.MirrorEdge(schema.DocumentsEdge)] = &xpb.EdgeSet_Group{ Edge: []*xpb.EdgeSet_Group_Edge{&xpb.EdgeSet_Group_Edge{TargetTicket: node.documented}}, } } if node.completes != "" { set.Groups[schema.CompletesEdge] = &xpb.EdgeSet_Group{ Edge: []*xpb.EdgeSet_Group_Edge{&xpb.EdgeSet_Group_Edge{TargetTicket: node.completes}}, } } if node.completed != "" { set.Groups[schema.MirrorEdge(schema.CompletesEdge)] = &xpb.EdgeSet_Group{ Edge: []*xpb.EdgeSet_Group_Edge{&xpb.EdgeSet_Group_Edge{TargetTicket: node.completed}}, } } if node.defines != "" { set.Groups[schema.DefinesBindingEdge] = &xpb.EdgeSet_Group{ Edge: []*xpb.EdgeSet_Group_Edge{&xpb.EdgeSet_Group_Edge{TargetTicket: node.defines}}, } } if node.params != nil { var edges []*xpb.EdgeSet_Group_Edge for i, p := range node.params { edges = append(edges, &xpb.EdgeSet_Group_Edge{TargetTicket: p, Ordinal: int32(i)}) } set.Groups[schema.ParamEdge] = &xpb.EdgeSet_Group{ Edge: edges, } } edges[node.ticket] = set } getNode := func(ticket string, facts []string) *xpb.NodeInfo { data, found := nodes[ticket] if !found { return nil } info := &xpb.NodeInfo{Facts: make(map[string][]byte)} for _, fact := range facts { info.Facts[fact] = data.Facts[fact] } return info } service := &mockService{ NodesFn: func(req *xpb.NodesRequest) (*xpb.NodesReply, error) { if len(req.Ticket) != 1 { t.Fatalf("Unexpected Nodes request: %v", req) return nil, nil } reply := &xpb.NodesReply{Nodes: make(map[string]*xpb.NodeInfo)} for _, ticket := range req.Ticket { if info := getNode(ticket, req.Filter); info != nil { reply.Nodes[ticket] = info } } return reply, nil }, EdgesFn: func(req *xpb.EdgesRequest) (*xpb.EdgesReply, error) { reply := &xpb.EdgesReply{ EdgeSets: make(map[string]*xpb.EdgeSet), Nodes: make(map[string]*xpb.NodeInfo), } for _, ticket := range req.Ticket { if data, found := edges[ticket]; found { set := &xpb.EdgeSet{Groups: make(map[string]*xpb.EdgeSet_Group)} reply.EdgeSets[ticket] = set nodes := make(map[string]bool) for groupKind, group := range data.Groups { for _, kind := range req.Kind { if groupKind == kind { set.Groups[kind] = group for _, edge := range group.Edge { if !nodes[edge.TargetTicket] { nodes[edge.TargetTicket] = true if node := getNode(edge.TargetTicket, req.Filter); node != nil { reply.Nodes[edge.TargetTicket] = node } } } break } } } } } return reply, nil }, CrossReferencesFn: func(req *xpb.CrossReferencesRequest) (*xpb.CrossReferencesReply, error) { if req.DefinitionKind != xpb.CrossReferencesRequest_ALL_DEFINITIONS || req.ReferenceKind != xpb.CrossReferencesRequest_NO_REFERENCES || req.DocumentationKind != xpb.CrossReferencesRequest_NO_DOCUMENTATION || req.AnchorText { t.Fatalf("Unexpected CrossReferences request: %v", req) } reply := &xpb.CrossReferencesReply{ CrossReferences: make(map[string]*xpb.CrossReferencesReply_CrossReferenceSet), } for _, ticket := range req.Ticket { if set, found := xrefs[ticket]; found { reply.CrossReferences[ticket] = set } } return reply, nil }, } for _, test := range tests { reply, err := SlowDocumentation(nil, service, &xpb.DocumentationRequest{Ticket: []string{test.ticket}}) if err != nil { t.Fatalf("SlowDocumentation error for %s: %v", test.ticket, err) } test.reply.Ticket = test.ticket if err := testutil.DeepEqual(&xpb.DocumentationReply{Document: []*xpb.DocumentationReply_Document{test.reply}}, reply); err != nil { t.Fatal(err) } } }
func displayEdgeGraph(reply *xpb.EdgesReply) error { nodes := xrefs.NodesMap(reply.Nodes) edges := make(map[string]map[string]stringset.Set) for source, es := range reply.EdgeSets { for gKind, g := range es.Groups { for _, edge := range g.Edge { tgt := edge.TargetTicket src, kind := source, gKind if schema.EdgeDirection(kind) == schema.Reverse { src, kind, tgt = tgt, schema.MirrorEdge(kind), src } groups, ok := edges[src] if !ok { groups = make(map[string]stringset.Set) edges[src] = groups } targets, ok := groups[kind] if !ok { targets = stringset.New() groups[kind] = targets } targets.Add(tgt) } } } if _, err := fmt.Println("digraph kythe {"); err != nil { return err } for ticket, node := range nodes { if _, err := fmt.Printf(` %q [label=<<table><tr><td colspan="2">%s</td></tr>`, ticket, html.EscapeString(ticket)); err != nil { return err } var facts []string for fact := range node { facts = append(facts, fact) } sort.Strings(facts) for _, fact := range facts { if _, err := fmt.Printf("<tr><td>%s</td><td>%s</td></tr>", html.EscapeString(fact), html.EscapeString(string(node[fact]))); err != nil { return err } } if _, err := fmt.Println("</table>> shape=plaintext];"); err != nil { return err } } if _, err := fmt.Println(); err != nil { return err } for src, groups := range edges { for kind, targets := range groups { for tgt := range targets { if _, err := fmt.Printf("\t%q -> %q [label=%q];\n", src, tgt, kind); err != nil { return err } } } } if _, err := fmt.Println("}"); err != nil { return err } return nil }
// Edges implements part of the xrefs.Service interface. func (db *DB) Edges(req *xpb.EdgesRequest) (*xpb.EdgesReply, error) { if req.PageSize != 0 || req.PageToken != "" { return nil, errors.New("edge pages unimplemented for SQL DB") } reply := &xpb.EdgesReply{} allowedKinds := stringset.New(req.Kind...) nodeTickets := stringset.New() for _, t := range req.Ticket { uri, err := kytheuri.Parse(t) if err != nil { return nil, err } edges := make(map[string]stringset.Set) var ( target kytheuri.URI kind string ) rows, err := db.edgesStmt.Query(uri.Signature, uri.Corpus, uri.Root, uri.Path, uri.Language) if err != nil { rows.Close() return nil, fmt.Errorf("forward edges query error: %v", err) } for rows.Next() { if err := rows.Scan(&target.Signature, &target.Corpus, &target.Root, &target.Path, &target.Language, &kind); err != nil { rows.Close() return nil, fmt.Errorf("forward edges scan error: %v", err) } else if len(allowedKinds) != 0 && !allowedKinds.Contains(kind) { continue } targets, ok := edges[kind] if !ok { targets = stringset.New() edges[kind] = targets } ticket := target.String() targets.Add(ticket) nodeTickets.Add(ticket) } if err := rows.Close(); err != nil { return nil, err } rows, err = db.revEdgesStmt.Query(uri.Signature, uri.Corpus, uri.Root, uri.Path, uri.Language) if err != nil { rows.Close() return nil, fmt.Errorf("reverse edges query error: %v", err) } for rows.Next() { if err := rows.Scan(&target.Signature, &target.Corpus, &target.Root, &target.Path, &target.Language, &kind); err != nil { rows.Close() return nil, fmt.Errorf("reverse edges scan error: %v", err) } kind = schema.MirrorEdge(kind) if len(allowedKinds) != 0 && !allowedKinds.Contains(kind) { continue } targets, ok := edges[kind] if !ok { targets = stringset.New() edges[kind] = targets } ticket := target.String() targets.Add(ticket) nodeTickets.Add(ticket) } if err := rows.Close(); err != nil { return nil, err } var g []*xpb.EdgeSet_Group for kind, targets := range edges { g = append(g, &xpb.EdgeSet_Group{ Kind: kind, TargetTicket: targets.Slice(), }) } if len(g) != 0 { reply.EdgeSet = append(reply.EdgeSet, &xpb.EdgeSet{ SourceTicket: t, Group: g, }) } } nodesReply, err := db.Nodes(&xpb.NodesRequest{ Ticket: nodeTickets.Slice(), Filter: req.Filter, }) if err != nil { return nil, fmt.Errorf("nodes request error: %v", err) } reply.Node = nodesReply.Node return reply, nil }