Skip to content

Commit 14ef93a

Browse files
authored
Introduce generic GetRecordValue API
1 parent 864d5d0 commit 14ef93a

File tree

3 files changed

+372
-0
lines changed

3 files changed

+372
-0
lines changed

neo4j/record.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* https://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package neo4j
21+
22+
import "fmt"
23+
24+
type RecordValue interface {
25+
bool | int64 | float64 | string |
26+
Point2D | Point3D |
27+
Date | LocalTime | LocalDateTime | Time | Duration | /* OffsetTime == Time == dbtype.Time */
28+
[]byte | []any | map[string]any |
29+
Node | Relationship | Path
30+
}
31+
32+
// GetRecordValue returns the value of the current provided record named by the specified key
33+
// The value type T must adhere to neo4j.RecordValue
34+
// If the key specified for the value does not exist, an error is returned
35+
// If the value is not defined for the provided existing key, the returned boolean is true
36+
// If the value type does not match the type specification, an error is returned
37+
//
38+
// Take this simple graph made of three nodes: `(:Person {name: "Arya"})`, `(:Person {name: ""})`, `(:Person)`
39+
// and the query: `MATCH (p:Person) RETURN p.name AS name`.
40+
// The following code illustrates when an error is returned vs. when the nil flag is set to true:
41+
// // the above query has been executed, `result` holds the cursor over the person nodes
42+
// for result.Next() {
43+
// record := result.Record()
44+
// name, isNil, err := neo4j.GetRecordValue[string](record, "name")
45+
// // for the person with the non-blank name, name == "Arya", isNil == false, err == nil
46+
// // for the person with the blank name, name == "", isNil == false, err == nil
47+
// // for the node without name, name == "", isNil == true, err == nil
48+
//
49+
// _, _, err := neo4j.GetRecordValue[string](record, "invalid-key")
50+
// // this results in an error, since "invalid-key" is not part of the query result keys
51+
// }
52+
func GetRecordValue[T RecordValue](record *Record, key string) (T, bool, error) {
53+
rawValue, found := record.Get(key)
54+
if !found {
55+
return *new(T), false, fmt.Errorf("record value %s not found", key)
56+
}
57+
if rawValue == nil {
58+
return *new(T), true, nil
59+
}
60+
value, ok := rawValue.(T)
61+
if !ok {
62+
zeroValue := *new(T)
63+
return zeroValue, false, fmt.Errorf("expected value to have type %T but found type %T", zeroValue, rawValue)
64+
}
65+
return value, false, nil
66+
}

neo4j/record_example_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* https://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package neo4j_test
21+
22+
import (
23+
"context"
24+
"fmt"
25+
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
26+
)
27+
28+
func ExampleGetRecordValue() {
29+
ctx := context.Background()
30+
driver, err := createDriver()
31+
handleError(err)
32+
defer handleClose(ctx, driver)
33+
session := driver.NewSession(ctx, neo4j.SessionConfig{})
34+
defer handleClose(ctx, session)
35+
36+
result, err := session.Run(ctx, "MATCH (p:Person) RETURN p LIMIT 1", nil)
37+
handleError(err)
38+
record, err := result.Single(ctx)
39+
handleError(err)
40+
41+
// GetRecordValue extracts the record value by the specified name
42+
// it also makes sure the value conforms to the specified type parameter
43+
// if a particular value is not present for the current record, isNil will be true
44+
personNode, isNil, err := neo4j.GetRecordValue[neo4j.Node](record, "p")
45+
handleError(err)
46+
if isNil {
47+
fmt.Println("no person found")
48+
} else {
49+
fmt.Println(personNode)
50+
}
51+
}

neo4j/record_test.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* https://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package neo4j_test
21+
22+
import (
23+
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
24+
. "github.com/neo4j/neo4j-go-driver/v5/neo4j/internal/testutil"
25+
"testing"
26+
"testing/quick"
27+
"time"
28+
)
29+
30+
func TestGetRecordValue(outer *testing.T) {
31+
outer.Parallel()
32+
33+
outer.Run("gets record value", func(t *testing.T) {
34+
checkValue := func(key string, value int64) bool {
35+
val, isNil, err := neo4j.GetRecordValue[int64](record(key, value), key)
36+
return value == val && !isNil && err == nil
37+
}
38+
checkNotFoundValue := func(key string, value int64) bool {
39+
_, isNil, err := neo4j.GetRecordValue[int64](record(key, value), key+"_nope")
40+
return !isNil && err.Error() == "record value "+key+"_nope not found"
41+
}
42+
checkIncompatibleValue := func(key string, value int64) bool {
43+
_, isNil, err := neo4j.GetRecordValue[string](record(key, value), key)
44+
return !isNil && err.Error() == "expected value to have type string but found type int64"
45+
}
46+
checkNilValue := func(key string) bool {
47+
_, isNil, err := neo4j.GetRecordValue[int64](record(key, nil), key)
48+
return isNil && err == nil
49+
}
50+
51+
for _, check := range []any{checkValue, checkNotFoundValue, checkIncompatibleValue, checkNilValue} {
52+
if err := quick.Check(check, nil); err != nil {
53+
outer.Error(err)
54+
}
55+
}
56+
})
57+
58+
outer.Run("supports only valid record values", func(inner *testing.T) {
59+
now := time.Now()
60+
61+
inner.Run("booleans", func(t *testing.T) {
62+
entity := record("k", true)
63+
64+
value, isNil, err := neo4j.GetRecordValue[bool](entity, "k")
65+
66+
AssertDeepEquals(t, value, true)
67+
AssertFalse(t, isNil)
68+
AssertNoError(t, err)
69+
})
70+
71+
inner.Run("longs", func(t *testing.T) {
72+
entity := record("k", int64(98))
73+
74+
value, isNil, err := neo4j.GetRecordValue[int64](entity, "k")
75+
76+
AssertDeepEquals(t, value, int64(98))
77+
AssertFalse(t, isNil)
78+
AssertNoError(t, err)
79+
})
80+
81+
inner.Run("doubles", func(t *testing.T) {
82+
entity := record("k", float64(99.42))
83+
84+
value, isNil, err := neo4j.GetRecordValue[float64](entity, "k")
85+
86+
AssertDeepEquals(t, value, float64(99.42))
87+
AssertFalse(t, isNil)
88+
AssertNoError(t, err)
89+
})
90+
91+
inner.Run("strings", func(t *testing.T) {
92+
entity := record("k", "v")
93+
94+
value, isNil, err := neo4j.GetRecordValue[string](entity, "k")
95+
96+
AssertDeepEquals(t, value, "v")
97+
AssertFalse(t, isNil)
98+
AssertNoError(t, err)
99+
})
100+
101+
inner.Run("point2Ds", func(t *testing.T) {
102+
entity := record("k", neo4j.Point2D{X: 3, Y: 14, SpatialRefId: 7203})
103+
104+
value, isNil, err := neo4j.GetRecordValue[neo4j.Point2D](entity, "k")
105+
106+
AssertDeepEquals(t, value, neo4j.Point2D{X: 3, Y: 14, SpatialRefId: 7203})
107+
AssertFalse(t, isNil)
108+
AssertNoError(t, err)
109+
})
110+
111+
inner.Run("point3Ds", func(t *testing.T) {
112+
entity := record("k", neo4j.Point3D{X: 3, Y: 1, Z: 4, SpatialRefId: 4979})
113+
114+
value, isNil, err := neo4j.GetRecordValue[neo4j.Point3D](entity, "k")
115+
116+
AssertDeepEquals(t, value, neo4j.Point3D{X: 3, Y: 1, Z: 4, SpatialRefId: 4979})
117+
AssertFalse(t, isNil)
118+
AssertNoError(t, err)
119+
})
120+
121+
inner.Run("dates", func(t *testing.T) {
122+
entity := record("k", neo4j.DateOf(now))
123+
124+
value, isNil, err := neo4j.GetRecordValue[neo4j.Date](entity, "k")
125+
126+
AssertDeepEquals(t, value, neo4j.DateOf(now))
127+
AssertFalse(t, isNil)
128+
AssertNoError(t, err)
129+
})
130+
131+
inner.Run("local times", func(t *testing.T) {
132+
entity := record("k", neo4j.LocalTimeOf(now))
133+
134+
value, isNil, err := neo4j.GetRecordValue[neo4j.LocalTime](entity, "k")
135+
136+
AssertDeepEquals(t, value, neo4j.LocalTimeOf(now))
137+
AssertFalse(t, isNil)
138+
AssertNoError(t, err)
139+
})
140+
141+
inner.Run("local datetimes", func(t *testing.T) {
142+
entity := record("k", neo4j.LocalDateTimeOf(now))
143+
144+
value, isNil, err := neo4j.GetRecordValue[neo4j.LocalDateTime](entity, "k")
145+
146+
AssertDeepEquals(t, value, neo4j.LocalDateTimeOf(now))
147+
AssertFalse(t, isNil)
148+
AssertNoError(t, err)
149+
})
150+
151+
inner.Run("times", func(t *testing.T) {
152+
entity := record("k", neo4j.Time(now))
153+
154+
value, isNil, err := neo4j.GetRecordValue[neo4j.Time](entity, "k")
155+
156+
AssertDeepEquals(t, value, neo4j.Time(now))
157+
AssertFalse(t, isNil)
158+
AssertNoError(t, err)
159+
})
160+
161+
inner.Run("offset times", func(t *testing.T) {
162+
entity := record("k", neo4j.OffsetTimeOf(now))
163+
164+
value, isNil, err := neo4j.GetRecordValue[neo4j.OffsetTime](entity, "k")
165+
166+
AssertDeepEquals(t, value, neo4j.OffsetTimeOf(now))
167+
AssertFalse(t, isNil)
168+
AssertNoError(t, err)
169+
})
170+
171+
inner.Run("durations", func(t *testing.T) {
172+
entity := record("k", neo4j.DurationOf(5, 4, 3, 2))
173+
174+
value, isNil, err := neo4j.GetRecordValue[neo4j.Duration](entity, "k")
175+
176+
AssertDeepEquals(t, value, neo4j.DurationOf(5, 4, 3, 2))
177+
AssertFalse(t, isNil)
178+
AssertNoError(t, err)
179+
})
180+
181+
inner.Run("byte arrays", func(t *testing.T) {
182+
entity := record("k", []byte{1, 2, 3})
183+
184+
value, isNil, err := neo4j.GetRecordValue[[]byte](entity, "k")
185+
186+
AssertDeepEquals(t, value, []byte{1, 2, 3})
187+
AssertFalse(t, isNil)
188+
AssertNoError(t, err)
189+
})
190+
191+
inner.Run("slices", func(t *testing.T) {
192+
entity := record("k", []any{1, 2, 3})
193+
194+
value, isNil, err := neo4j.GetRecordValue[[]any](entity, "k")
195+
196+
AssertDeepEquals(t, value, []any{1, 2, 3})
197+
AssertFalse(t, isNil)
198+
AssertNoError(t, err)
199+
})
200+
201+
inner.Run("maps", func(t *testing.T) {
202+
entity := record("k", map[string]any{"un": 1, "dos": 2, "tres": 3})
203+
204+
value, isNil, err := neo4j.GetRecordValue[map[string]any](entity, "k")
205+
206+
AssertDeepEquals(t, value, map[string]any{"un": 1, "dos": 2, "tres": 3})
207+
AssertFalse(t, isNil)
208+
AssertNoError(t, err)
209+
})
210+
211+
inner.Run("nodes", func(t *testing.T) {
212+
entity := record("k", neo4j.Node{Props: singleProp("n", 12)})
213+
214+
value, isNil, err := neo4j.GetRecordValue[neo4j.Node](entity, "k")
215+
216+
AssertDeepEquals(t, value, neo4j.Node{Props: singleProp("n", 12)})
217+
AssertFalse(t, isNil)
218+
AssertNoError(t, err)
219+
})
220+
221+
inner.Run("relationships", func(t *testing.T) {
222+
entity := record("k", neo4j.Relationship{Props: singleProp("n", 12)})
223+
224+
value, isNil, err := neo4j.GetRecordValue[neo4j.Relationship](entity, "k")
225+
226+
AssertDeepEquals(t, value, neo4j.Relationship{Props: singleProp("n", 12)})
227+
AssertFalse(t, isNil)
228+
AssertNoError(t, err)
229+
})
230+
231+
inner.Run("paths", func(t *testing.T) {
232+
entity := record("k", neo4j.Path{
233+
Nodes: []neo4j.Node{{Props: singleProp("n", 12)}},
234+
Relationships: []neo4j.Relationship{{Props: singleProp("m", 21)}},
235+
})
236+
237+
value, isNil, err := neo4j.GetRecordValue[neo4j.Path](entity, "k")
238+
239+
AssertDeepEquals(t, value, neo4j.Path{
240+
Nodes: []neo4j.Node{{Props: singleProp("n", 12)}},
241+
Relationships: []neo4j.Relationship{{Props: singleProp("m", 21)}},
242+
})
243+
AssertFalse(t, isNil)
244+
AssertNoError(t, err)
245+
})
246+
247+
})
248+
}
249+
250+
func record(key string, value any) *neo4j.Record {
251+
return &neo4j.Record{
252+
Values: []any{value},
253+
Keys: []string{key},
254+
}
255+
}

0 commit comments

Comments
 (0)