Skip to content

Commit fdd6b39

Browse files
authored
Add support for fetching properties in v3 node (#1456)
This is a special case where the response is not a LinkedGraph of nodes but instead a LinkedGraph of properties, so I created a different model (this corresponds to -> and <-, where no properties are specified) All remaining functionality will still use Edges and will return a LinkedGraph of nodes, which includes * SingleProp * Decorator * BracketProps * Filter * (Pagination)
1 parent 0d85c90 commit fdd6b39

File tree

10 files changed

+312
-29
lines changed

10 files changed

+312
-29
lines changed

internal/server/spanner/datasource.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,21 @@ func (sds *SpannerDataSource) Node(ctx context.Context, req *v3.NodeRequest) (*v
4949
if len(arcs) > 1 {
5050
return nil, fmt.Errorf("multiple arcs in node request")
5151
}
52-
edges, err := sds.client.GetNodeEdgesByID(ctx, req.Nodes, arcs[0])
53-
if err != nil {
54-
return nil, fmt.Errorf("error getting node edges: %v", err)
52+
arc := arcs[0]
53+
54+
if arc.SingleProp == "" && len(arc.BracketProps) == 0 {
55+
props, err := sds.client.GetNodeProps(ctx, req.Nodes, arc.Out)
56+
if err != nil {
57+
return nil, fmt.Errorf("error getting node properties: %v", err)
58+
}
59+
return nodePropsToNodeResponse(props), nil
60+
} else {
61+
edges, err := sds.client.GetNodeEdgesByID(ctx, req.Nodes, arc)
62+
if err != nil {
63+
return nil, fmt.Errorf("error getting node edges: %v", err)
64+
}
65+
return nodeEdgesToNodeResponse(edges), nil
5566
}
56-
return nodeEdgesToNodeResponse(edges), nil
5767
}
5868

5969
// Observation retrieves observation data from Spanner.

internal/server/spanner/dsutil.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@ import (
2222
v3 "github.com/datacommonsorg/mixer/internal/proto/v3"
2323
)
2424

25+
// nodePropsToNodeResponse converts a slice of properties to a NodeResponse proto.
26+
func nodePropsToNodeResponse(props []*Property) *v3.NodeResponse {
27+
nodeResponse := &v3.NodeResponse{
28+
Data: make(map[string]*v2.LinkedGraph),
29+
}
30+
31+
for _, prop := range props {
32+
linkedGraph, ok := nodeResponse.Data[prop.SubjectID]
33+
if !ok {
34+
linkedGraph = &v2.LinkedGraph{}
35+
nodeResponse.Data[prop.SubjectID] = linkedGraph
36+
}
37+
linkedGraph.Properties = append(linkedGraph.Properties, prop.Predicate)
38+
}
39+
40+
return nodeResponse
41+
}
42+
2543
// nodeEdgesToNodeResponse converts a map from subject id to its edges to a NodeResponse proto.
2644
func nodeEdgesToNodeResponse(edgesBySubjectID map[string][]*Edge) *v3.NodeResponse {
2745
nodeResponse := &v3.NodeResponse{
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"data": {
3+
"Count_Person": {
4+
"properties": [
5+
"extendedName",
6+
"localCuratorLevelId",
7+
"measuredProp",
8+
"memberOf",
9+
"name",
10+
"populationType",
11+
"statType",
12+
"typeOf"
13+
]
14+
},
15+
"Person": {
16+
"properties": [
17+
"equivalentClass",
18+
"extendedName",
19+
"localCuratorLevelId",
20+
"name",
21+
"source",
22+
"subClassOf",
23+
"typeOf"
24+
]
25+
}
26+
}
27+
}

internal/server/spanner/golden/datasource/node.json renamed to internal/server/spanner/golden/datasource/property_values.json

File renamed without changes.

internal/server/spanner/golden/datasource_test.go

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,33 +38,46 @@ func TestNode(t *testing.T) {
3838
ctx := context.Background()
3939
_, filename, _, _ := runtime.Caller(0)
4040
goldenDir := path.Join(path.Dir(filename), "datasource")
41-
goldenFile := "node.json"
4241

43-
req := &v3.NodeRequest{
44-
Nodes: []string{"Aadhaar", "Monthly_Average_RetailPrice_Electricity_Residential"},
45-
Property: "->*",
46-
}
47-
48-
got, err := ds.Node(ctx, req)
49-
if err != nil {
50-
t.Fatalf("Node error (%v): %v", goldenFile, err)
51-
}
42+
for _, c := range []struct {
43+
req *v3.NodeRequest
44+
goldenFile string
45+
}{
46+
{
47+
req: &v3.NodeRequest{
48+
Nodes: []string{"Count_Person", "Person"},
49+
Property: "->",
50+
},
51+
goldenFile: "properties.json",
52+
},
53+
{
54+
req: &v3.NodeRequest{
55+
Nodes: []string{"Aadhaar", "Monthly_Average_RetailPrice_Electricity_Residential"},
56+
Property: "->*",
57+
},
58+
goldenFile: "property_values.json",
59+
},
60+
} {
61+
got, err := ds.Node(ctx, c.req)
62+
if err != nil {
63+
t.Fatalf("Node error (%v): %v", c.goldenFile, err)
64+
}
5265

53-
if test.GenerateGolden {
54-
test.UpdateProtoGolden(got, goldenDir, goldenFile)
55-
return
56-
}
66+
if test.GenerateGolden {
67+
test.UpdateProtoGolden(got, goldenDir, c.goldenFile)
68+
return
69+
}
5770

58-
var want v3.NodeResponse
59-
if err = test.ReadJSON(goldenDir, goldenFile, &want); err != nil {
60-
t.Fatalf("ReadJSON error (%v): %v", goldenFile, err)
61-
}
71+
var want v3.NodeResponse
72+
if err = test.ReadJSON(goldenDir, c.goldenFile, &want); err != nil {
73+
t.Fatalf("ReadJSON error (%v): %v", c.goldenFile, err)
74+
}
6275

63-
cmpOpts := cmp.Options{
64-
protocmp.Transform(),
76+
cmpOpts := cmp.Options{
77+
protocmp.Transform(),
78+
}
79+
if diff := cmp.Diff(got, &want, cmpOpts); diff != "" {
80+
t.Errorf("%v payload mismatch:\n%v", c.goldenFile, diff)
81+
}
6582
}
66-
if diff := cmp.Diff(got, &want, cmpOpts); diff != "" {
67-
t.Errorf("%v payload mismatch:\n%v", goldenFile, diff)
68-
}
69-
7083
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[
2+
{
3+
"SubjectID": "Count_Person",
4+
"Predicate": "measurementDenominator"
5+
},
6+
{
7+
"SubjectID": "Person",
8+
"Predicate": "domainIncludes"
9+
},
10+
{
11+
"SubjectID": "Person",
12+
"Predicate": "populationType"
13+
},
14+
{
15+
"SubjectID": "Person",
16+
"Predicate": "rangeIncludes"
17+
},
18+
{
19+
"SubjectID": "Person",
20+
"Predicate": "subClassOf"
21+
},
22+
{
23+
"SubjectID": "Person",
24+
"Predicate": "unitOfMeasure"
25+
},
26+
{
27+
"SubjectID": "Person",
28+
"Predicate": "victimType"
29+
}
30+
]
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
[
2+
{
3+
"SubjectID": "Count_Person",
4+
"Predicate": "extendedName"
5+
},
6+
{
7+
"SubjectID": "Count_Person",
8+
"Predicate": "localCuratorLevelId"
9+
},
10+
{
11+
"SubjectID": "Count_Person",
12+
"Predicate": "measuredProp"
13+
},
14+
{
15+
"SubjectID": "Count_Person",
16+
"Predicate": "memberOf"
17+
},
18+
{
19+
"SubjectID": "Count_Person",
20+
"Predicate": "name"
21+
},
22+
{
23+
"SubjectID": "Count_Person",
24+
"Predicate": "populationType"
25+
},
26+
{
27+
"SubjectID": "Count_Person",
28+
"Predicate": "statType"
29+
},
30+
{
31+
"SubjectID": "Count_Person",
32+
"Predicate": "typeOf"
33+
},
34+
{
35+
"SubjectID": "Person",
36+
"Predicate": "equivalentClass"
37+
},
38+
{
39+
"SubjectID": "Person",
40+
"Predicate": "extendedName"
41+
},
42+
{
43+
"SubjectID": "Person",
44+
"Predicate": "localCuratorLevelId"
45+
},
46+
{
47+
"SubjectID": "Person",
48+
"Predicate": "name"
49+
},
50+
{
51+
"SubjectID": "Person",
52+
"Predicate": "source"
53+
},
54+
{
55+
"SubjectID": "Person",
56+
"Predicate": "subClassOf"
57+
},
58+
{
59+
"SubjectID": "Person",
60+
"Predicate": "typeOf"
61+
}
62+
]

internal/server/spanner/golden/query_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,62 @@ import (
2626
"github.com/google/go-cmp/cmp"
2727
)
2828

29+
func TestGetNodeProps(t *testing.T) {
30+
client := test.NewSpannerClient()
31+
if client == nil {
32+
return
33+
}
34+
35+
t.Parallel()
36+
ctx := context.Background()
37+
_, filename, _, _ := runtime.Caller(0)
38+
goldenDir := path.Join(path.Dir(filename), "query")
39+
40+
for _, c := range []struct {
41+
ids []string
42+
out bool
43+
goldenFile string
44+
}{
45+
{
46+
ids: []string{"Count_Person", "Person"},
47+
out: true,
48+
goldenFile: "get_node_props_by_subject_id.json",
49+
},
50+
{
51+
ids: []string{"Count_Person", "Person"},
52+
out: false,
53+
goldenFile: "get_node_props_by_object_id.json",
54+
},
55+
} {
56+
actual, err := client.GetNodeProps(ctx, c.ids, c.out)
57+
if err != nil {
58+
t.Fatalf("GetNodeProps error (%v): %v", c.goldenFile, err)
59+
}
60+
61+
got, err := test.StructToJSON(actual)
62+
if err != nil {
63+
t.Fatalf("StructToJSON error (%v): %v", c.goldenFile, err)
64+
}
65+
66+
if test.GenerateGolden {
67+
err = test.WriteGolden(got, goldenDir, c.goldenFile)
68+
if err != nil {
69+
t.Fatalf("WriteGolden error (%v): %v", c.goldenFile, err)
70+
}
71+
return
72+
}
73+
74+
want, err := test.ReadGolden(goldenDir, c.goldenFile)
75+
if err != nil {
76+
t.Fatalf("ReadGolden error (%v): %v", c.goldenFile, err)
77+
}
78+
79+
if diff := cmp.Diff(want, got); diff != "" {
80+
t.Errorf("%v payload mismatch (-want +got):\n%s", c.goldenFile, diff)
81+
}
82+
}
83+
}
84+
2985
func TestGetNodeEdgesByID(t *testing.T) {
3086
client := test.NewSpannerClient()
3187
if client == nil {

internal/server/spanner/model.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
// Model objects related to the spanner graph database.
1616
package spanner
1717

18+
// Property struct represents a subset of a row in the Edge table.
19+
type Property struct {
20+
SubjectID string `spanner:"subject_id"`
21+
Predicate string `spanner:"predicate"`
22+
}
23+
1824
// Edge struct represents a single row in the Edge table, supplemented with the object name and types.
1925
type Edge struct {
2026
SubjectID string `spanner:"subject_id"`

0 commit comments

Comments
 (0)