Skip to content

Commit 35b236e

Browse files
Fix comparison of numeric jsonb columns
Since we use ->> on jsonb fields we always get a string back. Comparisons such as < and > were done on string values which gives unexpected results.
1 parent 947a271 commit 35b236e

File tree

5 files changed

+81
-28
lines changed

5 files changed

+81
-28
lines changed

examples/readme_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ func ExampleNewConverter_readme() {
3333
fmt.Println(conditions)
3434
fmt.Printf("%#v\n", values)
3535
// Output:
36-
// ((("meta"->>'map' ~* $1) OR ("meta"->>'map' ~* $2)) AND ("meta"->>'password' = $3) AND (("meta"->>'playerCount' >= $4) AND ("meta"->>'playerCount' < $5)))
36+
// ((("meta"->>'map' ~* $1) OR ("meta"->>'map' ~* $2)) AND ("meta"->>'password' = $3) AND ((("meta"->>'playerCount')::numeric >= $4) AND (("meta"->>'playerCount')::numeric < $5)))
3737
// []interface {}{"aztec", "nuke", "", 2, 10}
3838
}

filter/converter.go

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import (
1010
"sync"
1111
)
1212

13-
var basicOperatorMap = map[string]string{
14-
"$gt": ">",
15-
"$gte": ">=",
16-
"$lt": "<",
17-
"$lte": "<=",
13+
var numericOperatorMap = map[string]string{
14+
"$gt": ">",
15+
"$gte": ">=",
16+
"$lt": "<",
17+
"$lte": "<=",
18+
}
19+
20+
var textOperatorMap = map[string]string{
1821
"$eq": "=",
1922
"$ne": "!=",
2023
"$regex": "~*",
@@ -200,14 +203,7 @@ func (c *Converter) convertFilter(filter map[string]any, paramIndex int) (string
200203
values = append(values, v[operator])
201204
case "$exists":
202205
// $exists only works on jsonb columns, so we need to check if the key is in the JSONB data first.
203-
isNestedColumn := c.nestedColumn != ""
204-
for _, exemption := range c.nestedExemptions {
205-
if exemption == key {
206-
isNestedColumn = false
207-
break
208-
}
209-
}
210-
if !isNestedColumn {
206+
if !c.isNestedColumn(key) {
211207
// There is no way in Postgres to check if a column exists on a table.
212208
return "", nil, fmt.Errorf("$exists operator not supported on non-nested jsonb columns")
213209
}
@@ -217,20 +213,14 @@ func (c *Converter) convertFilter(filter map[string]any, paramIndex int) (string
217213
}
218214
inner = append(inner, fmt.Sprintf("(%sjsonb_path_match(%s, 'exists($.%s)'))", neg, c.nestedColumn, key))
219215
case "$elemMatch":
220-
// $elemMatch needs a different implementation depending on if the column is in JSONB or not.
221-
isNestedColumn := c.nestedColumn != ""
222-
for _, exemption := range c.nestedExemptions {
223-
if exemption == key {
224-
isNestedColumn = false
225-
break
226-
}
227-
}
228216
innerConditions, innerValues, err := c.convertFilter(map[string]any{c.placeholderName: v[operator]}, paramIndex)
229217
if err != nil {
230218
return "", nil, err
231219
}
232220
paramIndex += len(innerValues)
233-
if isNestedColumn {
221+
222+
// $elemMatch needs a different implementation depending on if the column is in JSONB or not.
223+
if c.isNestedColumn(key) {
234224
// This will for example become:
235225
//
236226
// EXISTS (SELECT 1 FROM jsonb_array_elements("meta"->'foo') AS __filter_placeholder WHERE ("__filter_placeholder"::text = $1))
@@ -247,11 +237,29 @@ func (c *Converter) convertFilter(filter map[string]any, paramIndex int) (string
247237
values = append(values, innerValues...)
248238
default:
249239
value := v[operator]
250-
op, ok := basicOperatorMap[operator]
240+
isNumericOperatorMap := false
241+
op, ok := textOperatorMap[operator]
251242
if !ok {
252-
return "", nil, fmt.Errorf("unknown operator: %s", operator)
243+
op, ok = numericOperatorMap[operator]
244+
if !ok {
245+
return "", nil, fmt.Errorf("unknown operator: %s", operator)
246+
}
247+
isNumericOperatorMap = true
248+
}
249+
250+
if !isScalar(value) {
251+
return "", nil, fmt.Errorf("invalid comparison value (must be a primitive): %v", value)
252+
}
253+
254+
if isNumericOperatorMap && isNumeric(value) {
255+
if c.isNestedColumn(key) {
256+
inner = append(inner, fmt.Sprintf("((%s)::numeric %s $%d)", c.columnName(key), op, paramIndex))
257+
} else {
258+
inner = append(inner, fmt.Sprintf("(%s %s $%d)", c.columnName(key), op, paramIndex))
259+
}
260+
} else {
261+
inner = append(inner, fmt.Sprintf("(%s %s $%d)", c.columnName(key), op, paramIndex))
253262
}
254-
inner = append(inner, fmt.Sprintf("(%s %s $%d)", c.columnName(key), op, paramIndex))
255263
paramIndex++
256264
values = append(values, value)
257265
}
@@ -308,3 +316,15 @@ func (c *Converter) columnName(column string) string {
308316
}
309317
return fmt.Sprintf(`%q->>'%s'`, c.nestedColumn, column)
310318
}
319+
320+
func (c *Converter) isNestedColumn(column string) bool {
321+
if c.nestedColumn == "" {
322+
return false
323+
}
324+
for _, exemption := range c.nestedExemptions {
325+
if exemption == column {
326+
return false
327+
}
328+
}
329+
return true
330+
}

filter/converter_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,14 @@ func TestConverter_Convert(t *testing.T) {
358358
[]any{float64(18)},
359359
nil,
360360
},
361+
{
362+
"numeric comparison bug with jsonb column",
363+
filter.WithNestedJSONB("meta"),
364+
`{"foo": {"$gt": 0}}`,
365+
`(("meta"->>'foo')::numeric > $1)`,
366+
[]any{float64(0)},
367+
nil,
368+
},
361369
}
362370

363371
for _, tt := range tests {

filter/util.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
package filter
22

3+
func isNumeric(v any) bool {
4+
if v == nil {
5+
return true
6+
}
7+
8+
switch v.(type) {
9+
case float64:
10+
return true
11+
default:
12+
return false
13+
}
14+
}
15+
316
func isScalar(v any) bool {
417
if v == nil {
518
return true

integration/postgres_test.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,16 @@ func TestIntegration_BasicOperators(t *testing.T) {
286286
nil,
287287
},
288288
{
289-
`invalid value`,
289+
`invalid value type 1`,
290290
`{"level": "town1"}`, // Level is an integer column, but the value is a string.
291291
nil,
292-
errors.New("pq: invalid input syntax for type integer: \"town1\""),
292+
errors.New(`pq: invalid input syntax for type integer: "town1"`),
293+
},
294+
{
295+
`invalid value type 2`,
296+
`{"name": 123}`, // Name is a string column, but the value is an integer.
297+
[]int{},
298+
nil,
293299
},
294300
{
295301
`empty object`,
@@ -381,6 +387,12 @@ func TestIntegration_BasicOperators(t *testing.T) {
381387
[]int{3},
382388
nil,
383389
},
390+
{
391+
"numeric comparison bug bug with jsonb column",
392+
`{"guild_id": {"$lt": 100}}`,
393+
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, // wrong
394+
nil,
395+
},
384396
}
385397

386398
for _, tt := range tests {

0 commit comments

Comments
 (0)