Skip to content

Commit b7abc99

Browse files
authored
feat: Add support for _eq and _neq to inline scalar arrays (#4293)
## Relevant issue(s) Resolves #4280 ## Description Adds support for `_eq` and `_ne` to inline arrays and nonnull inline arrays. ## Tasks - [x] I made sure the code is well commented, particularly hard-to-understand areas. - [x] I made sure the pull request title adheres to the conventional commit style (the subset used in the project can be found in [tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)). - [x] I made sure to discuss its limitations such as threats to validity, vulnerability to mistake and misuse, robustness to invalidation of assumptions, resource requirements, ... ## How has this been tested? Notable expansion of the inline_array integration tests to cover all current inline array scalar types with both `_eq` and `_ne`. Specify the platform(s) on which this was tested: - Ubuntu (WSL2)
1 parent 8b50635 commit b7abc99

7 files changed

Lines changed: 937 additions & 20 deletions

File tree

internal/connor/eq.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ func eq(condition, data any, propExists bool) (bool, error) {
7373
case time.Time:
7474
return ctime.Equal(cn, data), nil
7575

76+
case []any:
77+
equal, err := equalAnyToAnySlice(data, cn)
78+
if err != nil {
79+
return reflect.DeepEqual(condition, data), nil
80+
}
81+
return equal, nil
82+
7683
default:
7784
return reflect.DeepEqual(condition, data), nil
7885
}

internal/connor/eq_slice.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package connor
2+
3+
import (
4+
"cmp"
5+
"time"
6+
7+
"github.com/sourcenetwork/immutable"
8+
9+
"github.com/sourcenetwork/defradb/internal/connor/numbers"
10+
)
11+
12+
func equalAnyToAnySlice(a any, b []any) (bool, error) {
13+
switch aTyped := a.(type) {
14+
case []bool:
15+
return equalSlice(aTyped, b), nil
16+
case []int:
17+
return equalSliceNumeric(aTyped, b), nil
18+
case []int32:
19+
return equalSliceNumeric(aTyped, b), nil
20+
case []int64:
21+
return equalSliceNumeric(aTyped, b), nil
22+
case []float32:
23+
return equalSliceNumeric(aTyped, b), nil
24+
case []float64:
25+
return equalSliceNumeric(aTyped, b), nil
26+
case []string:
27+
return equalSlice(aTyped, b), nil
28+
case []time.Time:
29+
return equalSliceTime(aTyped, b), nil
30+
31+
case []immutable.Option[bool]:
32+
return equalOptionSlice(aTyped, b), nil
33+
case []immutable.Option[int]:
34+
return equalOptionSliceNumeric(aTyped, b), nil
35+
case []immutable.Option[int32]:
36+
return equalOptionSliceNumeric(aTyped, b), nil
37+
case []immutable.Option[int64]:
38+
return equalOptionSliceNumeric(aTyped, b), nil
39+
case []immutable.Option[float32]:
40+
return equalOptionSliceNumeric(aTyped, b), nil
41+
case []immutable.Option[float64]:
42+
return equalOptionSliceNumeric(aTyped, b), nil
43+
case []immutable.Option[string]:
44+
return equalOptionSlice(aTyped, b), nil
45+
case []immutable.Option[time.Time]:
46+
return equalOptionSliceTime(aTyped, b), nil
47+
default:
48+
return false, ErrSliceTypeNotFound
49+
}
50+
}
51+
52+
// This is basically the most effecient approach to slice comparison
53+
// since it avoids 1) reflection 2) allocation
54+
func equalSlice[T comparable](a []T, b any) bool {
55+
switch bTyped := b.(type) {
56+
case []T:
57+
if len(a) != len(bTyped) {
58+
return false
59+
}
60+
for i, v := range a {
61+
if v != bTyped[i] {
62+
return false
63+
}
64+
}
65+
return true
66+
case []any:
67+
if len(a) != len(bTyped) {
68+
return false
69+
}
70+
for i, v := range a {
71+
if bv, ok := bTyped[i].(T); !ok || v != bv {
72+
return false
73+
}
74+
}
75+
return true
76+
default:
77+
return false
78+
}
79+
}
80+
81+
// This is basically the most effecient approach to slice comparison
82+
// since it avoids 1) reflection 2) allocation
83+
func equalSliceNumeric[T cmp.Ordered](a []T, b any) bool {
84+
switch bTyped := b.(type) {
85+
case []T:
86+
if len(a) != len(bTyped) {
87+
return false
88+
}
89+
for i, v := range a {
90+
if v != bTyped[i] {
91+
return false
92+
}
93+
}
94+
return true
95+
case []any:
96+
if len(a) != len(bTyped) {
97+
return false
98+
}
99+
for i, v := range a {
100+
bv, ok := bTyped[i].(T)
101+
if !ok && !numbers.Equal(v, bTyped[i]) {
102+
return false
103+
} else if ok && v != bv {
104+
return false
105+
}
106+
}
107+
return true
108+
default:
109+
return false
110+
}
111+
}
112+
113+
// equalSliceTime compares time.Time slices using the Equal method
114+
// which correctly handles timezone differences.
115+
func equalSliceTime(a []time.Time, b any) bool {
116+
switch bTyped := b.(type) {
117+
case []time.Time:
118+
if len(a) != len(bTyped) {
119+
return false
120+
}
121+
for i, v := range a {
122+
if !v.Equal(bTyped[i]) {
123+
return false
124+
}
125+
}
126+
return true
127+
case []any:
128+
if len(a) != len(bTyped) {
129+
return false
130+
}
131+
for i, v := range a {
132+
if bv, ok := bTyped[i].(time.Time); !ok || !v.Equal(bv) {
133+
return false
134+
}
135+
}
136+
return true
137+
default:
138+
return false
139+
}
140+
}
141+
142+
func equalOption[T comparable](a, b immutable.Option[T]) bool {
143+
if !a.HasValue() && !b.HasValue() {
144+
return true
145+
}
146+
if a.HasValue() != b.HasValue() {
147+
return false
148+
}
149+
return a.Value() == b.Value()
150+
}
151+
152+
func equalOptionSlice[T comparable](a []immutable.Option[T], b any) bool {
153+
switch bTyped := b.(type) {
154+
case []immutable.Option[T]:
155+
if len(a) != len(bTyped) {
156+
return false
157+
}
158+
for i, v := range a {
159+
if !equalOption(v, bTyped[i]) {
160+
return false
161+
}
162+
}
163+
return true
164+
case []any:
165+
if len(a) != len(bTyped) {
166+
return false
167+
}
168+
for i, v := range a {
169+
hasVal := v.HasValue()
170+
if !hasVal && bTyped[i] == nil {
171+
continue
172+
} else if hasVal && bTyped[i] == v.Value() {
173+
continue
174+
}
175+
return false
176+
}
177+
return true
178+
default:
179+
return false
180+
}
181+
}
182+
183+
func equalOptionSliceNumeric[T cmp.Ordered](a []immutable.Option[T], b any) bool {
184+
switch bTyped := b.(type) {
185+
case []immutable.Option[T]:
186+
if len(a) != len(bTyped) {
187+
return false
188+
}
189+
for i, v := range a {
190+
if !equalOption(v, bTyped[i]) {
191+
return false
192+
}
193+
}
194+
return true
195+
case []any:
196+
if len(a) != len(bTyped) {
197+
return false
198+
}
199+
for i, v := range a {
200+
hasVal := v.HasValue()
201+
if !hasVal && bTyped[i] == nil {
202+
continue
203+
} else if hasVal && numbers.Equal(bTyped[i], v.Value()) {
204+
continue
205+
}
206+
return false
207+
}
208+
return true
209+
default:
210+
return false
211+
}
212+
}
213+
214+
// equalOptionSliceTime compares immutable.Option[time.Time] slices using the Equal method
215+
// which correctly handles timezone differences.
216+
func equalOptionSliceTime(a []immutable.Option[time.Time], b any) bool {
217+
switch bTyped := b.(type) {
218+
case []immutable.Option[time.Time]:
219+
if len(a) != len(bTyped) {
220+
return false
221+
}
222+
for i, v := range a {
223+
if !equalOptionTime(v, bTyped[i]) {
224+
return false
225+
}
226+
}
227+
return true
228+
case []any:
229+
if len(a) != len(bTyped) {
230+
return false
231+
}
232+
for i, v := range a {
233+
hasVal := v.HasValue()
234+
if !hasVal && bTyped[i] == nil {
235+
continue
236+
} else if hasVal {
237+
if bv, ok := bTyped[i].(time.Time); ok && v.Value().Equal(bv) {
238+
continue
239+
}
240+
}
241+
return false
242+
}
243+
return true
244+
default:
245+
return false
246+
}
247+
}
248+
249+
func equalOptionTime(a, b immutable.Option[time.Time]) bool {
250+
if !a.HasValue() && !b.HasValue() {
251+
return true
252+
}
253+
if a.HasValue() != b.HasValue() {
254+
return false
255+
}
256+
return a.Value().Equal(b.Value())
257+
}

internal/connor/errors.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const (
2323
// This list is incomplete and undefined errors may also be returned.
2424
// Errors returned from this package may be tested against these errors with errors.Is.
2525
var (
26-
ErrUnknownOperator = errors.New(errUnknownOperator)
26+
ErrUnknownOperator = errors.New(errUnknownOperator)
27+
ErrSliceTypeNotFound = errors.New("slice type not found")
2728
)
2829

2930
func NewErrUnknownOperator(operator string) error {

internal/connor/time/equality.go

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,22 @@ package time
22

33
import "time"
44

5-
func Equal(condition, data any) bool {
6-
switch c := condition.(type) {
5+
func Equal(condition time.Time, data any) bool {
6+
switch d := data.(type) {
77
case time.Time:
8-
switch d := data.(type) {
9-
case time.Time:
10-
return d.Equal(c)
11-
case string:
12-
// todo: Not sure if we should be
13-
// parsing incoming data here, or
14-
// if the DB should handle this.
15-
// (Note: This isnt the user provided
16-
// condition on a request, but the data
17-
// stored in DB for a document
18-
dt, err := time.Parse(time.RFC3339, d)
19-
if err != nil {
20-
return false
21-
}
22-
return dt.Equal(c)
23-
default:
8+
return d.Equal(condition)
9+
case string:
10+
// todo: Not sure if we should be
11+
// parsing incoming data here, or
12+
// if the DB should handle this.
13+
// (Note: This isnt the user provided
14+
// condition on a request, but the data
15+
// stored in DB for a document
16+
dt, err := time.Parse(time.RFC3339, d)
17+
if err != nil {
2418
return false
2519
}
20+
return dt.Equal(condition)
2621
default:
2722
return false
2823
}

0 commit comments

Comments
 (0)