diff --git a/go/mysql/json/json_path.go b/go/mysql/json/json_path.go index c62a8c7b1c2..693bde0ba07 100644 --- a/go/mysql/json/json_path.go +++ b/go/mysql/json/json_path.go @@ -41,6 +41,7 @@ const ( jpMemberAny jpArrayLocation jpArrayLocationAny + jpArrayLocationRange jpAny ) @@ -71,6 +72,15 @@ func (jp *Path) format(b *strings.Builder) { case jpMemberAny: b.WriteString(".*") case jpArrayLocation: + switch { + case jp.offset0 == -1: + b.WriteString("[last]") + case jp.offset0 >= 0: + _, _ = fmt.Fprintf(b, "[%d]", jp.offset0) + case jp.offset0 < 0: + _, _ = fmt.Fprintf(b, "[last%d]", jp.offset0+1) + } + case jpArrayLocationRange: switch { case jp.offset0 == -1: b.WriteString("[last") @@ -82,7 +92,7 @@ func (jp *Path) format(b *strings.Builder) { switch { case jp.offset1 == -1: b.WriteString(" to last") - case jp.offset1 > 0: + case jp.offset1 >= 0: _, _ = fmt.Fprintf(b, " to %d", jp.offset1) case jp.offset1 < 0: _, _ = fmt.Fprintf(b, " to last%d", jp.offset1+1) @@ -107,12 +117,8 @@ func (jp *Path) String() string { func (jp *Path) ContainsWildcards() bool { for jp != nil { switch jp.kind { - case jpAny, jpArrayLocationAny, jpMemberAny: + case jpAny, jpArrayLocationAny, jpArrayLocationRange, jpMemberAny: return true - case jpArrayLocation: - if jp.offset1 != 0 { - return true - } } jp = jp.next } @@ -186,6 +192,15 @@ func (m *matcher) value(p *Path, v *Value) { }) } case jpArrayLocation: + if ary, ok := v.Array(); ok { + from, _ := p.arrayOffsets(ary) + if from >= 0 && from < len(ary) { + m.value(p.next, ary[from]) + } + } else if m.wrap && (p.offset0 == 0 || p.offset0 == -1) { + m.value(p.next, v) + } + case jpArrayLocationRange: if ary, ok := v.Array(); ok { from, to := p.arrayOffsets(ary) if from >= 0 && from < len(ary) { @@ -196,7 +211,7 @@ func (m *matcher) value(p *Path, v *Value) { m.value(p.next, ary[n]) } } - } else if m.wrap && (p.offset0 == 0 || p.offset0 == -1) { + } else if m.wrap && (p.offset0 == 0 || p.offset1 == -1) { m.value(p.next, v) } case jpArrayLocationAny: @@ -233,6 +248,20 @@ func (jp *Path) transform(v *Value, t func(pp *Path, vv *Value)) { jp.next.transform(obj.Get(jp.name), t) } case jpArrayLocation: + if ary, ok := v.Array(); ok { + from, _ := jp.arrayOffsets(ary) + if from >= 0 && from < len(ary) { + jp.next.transform(ary[from], t) + } + } else if jp.offset0 == 0 || jp.offset0 == -1 { + /* + If the path is evaluated against a value that is not an array, + the result of the evaluation is the same as if the value had been + wrapped in a single-element array: + */ + jp.next.transform(v, t) + } + case jpArrayLocationRange: if ary, ok := v.Array(); ok { from, to := jp.arrayOffsets(ary) if from != to { @@ -241,7 +270,7 @@ func (jp *Path) transform(v *Value, t func(pp *Path, vv *Value)) { if from >= 0 && from < len(ary) { jp.next.transform(ary[from], t) } - } else if jp.offset0 == 0 || jp.offset0 == -1 { + } else if jp.offset0 == 0 || jp.offset1 == -1 { /* If the path is evaluated against a value that is not an array, the result of the evaluation is the same as if the value had been @@ -532,13 +561,17 @@ func stepArrayLocationTo(p *PathParser, in []byte) ([]byte, error) { if in == nil || skip == 0 { return nil, errInvalid } + + // Upgrade to range + p.path.kind = jpArrayLocationRange + if in[0] >= '0' && in[0] <= '9' { p.step = stepArrayLocationClose offset, in2, err := p.lexNumeric(in) if err != nil { return nil, err } - if offset <= p.path.offset0 { + if offset < p.path.offset0 { return nil, fmt.Errorf("range %d should be >= %d", offset, p.path.offset0) } p.path.offset1 = offset diff --git a/go/mysql/json/json_path_test.go b/go/mysql/json/json_path_test.go index 63313b55ac3..54bf7730431 100644 --- a/go/mysql/json/json_path_test.go +++ b/go/mysql/json/json_path_test.go @@ -17,8 +17,10 @@ limitations under the License. package json import ( - "slices" + "fmt" "testing" + + "github.com/stretchr/testify/require" ) func TestParseJSONPath(t *testing.T) { @@ -38,6 +40,8 @@ func TestParseJSONPath(t *testing.T) { {P: `$.c[last-23 to last-444]`}, {P: `$.c[last-23 to last]`}, {P: `$.c[last]`}, + {P: `$.c[last to last]`}, + {P: `$.c[0 to 0]`}, {P: `$.c[last - 23 to last]`, Want: `$.c[last-23 to last]`}, {P: `$ . c [ last - 23 to last - 444 ]`, Want: `$.c[last-23 to last-444]`}, {P: `$. "a fish" `, Want: `$."a fish"`}, @@ -45,28 +49,24 @@ func TestParseJSONPath(t *testing.T) { } for _, tc := range cases { - var p PathParser - jp, err := p.ParseBytes([]byte(tc.P)) - if tc.Err == "" { - if err != nil { - t.Fatalf("failed to parse '%s': %v", tc.P, err) + t.Run(tc.P, func(t *testing.T) { + var p PathParser + jp, err := p.ParseBytes([]byte(tc.P)) + + if tc.Err != "" { + require.EqualError(t, err, tc.Err) + return } - } else { - if err == nil { - t.Fatalf("bad parse for '%s': expected an error", tc.P) - } else if err.Error() != tc.Err { - t.Fatalf("bad parse for '%s': expected err='%s', got err='%s'", tc.P, tc.Err, err) + + require.NoError(t, err) + + want := tc.Want + if want == "" { + want = tc.P } - continue - } - want := tc.Want - if want == "" { - want = tc.P - } - got := jp.String() - if got != want { - t.Fatalf("bad parse for '%s': want '%s', got '%s'", tc.P, want, got) - } + + require.Equal(t, want, jp.String()) + }) } } @@ -88,8 +88,14 @@ func TestJSONExtract(t *testing.T) { {`{"a": 1, "b": 2, "c": [3, 4, 5]}`, `$.*`, []string{"1", "2", "[3, 4, 5]"}}, {`true`, `$[0]`, []string{"true"}}, {`true`, `$[last]`, []string{"true"}}, - {`true`, `$[1]`, []string{}}, - {`true`, `$[last-1]`, []string{}}, + {`true`, `$[0 to 0]`, []string{"true"}}, + {`true`, `$[0 to 1]`, []string{"true"}}, + {`true`, `$[1 to 2]`, nil}, + {`true`, `$[last to last]`, []string{"true"}}, + {`true`, `$[last-4 to last]`, []string{"true"}}, + {`true`, `$[last-4 to last-1]`, nil}, + {`true`, `$[1]`, nil}, + {`true`, `$[last-1]`, nil}, {`[ { "a": 1 }, { "a": 2 } ]`, `$**[0]`, []string{`{"a": 1}`, `1`, `{"a": 2}`, `2`}}, {`{ "a" : "foo", "b" : [ true, { "c" : 123, "c" : 456 } ] }`, `$.b[1].c`, []string{"456"}}, {`{ "a" : "foo", "b" : [ true, { "c" : "123" } ] }`, `$.b[1].c`, []string{"\"123\""}}, @@ -97,20 +103,18 @@ func TestJSONExtract(t *testing.T) { } for _, tc := range cases { - var matched []string - err := MatchPath([]byte(tc.J), []byte(tc.JP), func(value *Value) { - if value == nil { - return - } - matched = append(matched, string(value.MarshalTo(nil))) + t.Run(fmt.Sprintf("'%s' -> '%s'", tc.J, tc.JP), func(t *testing.T) { + var matched []string + err := MatchPath([]byte(tc.J), []byte(tc.JP), func(value *Value) { + if value == nil { + return + } + matched = append(matched, string(value.MarshalTo(nil))) + }) + + require.NoError(t, err) + require.Equal(t, tc.Expected, matched) }) - if err != nil { - t.Errorf("failed to match '%s'->'%s': %v", tc.J, tc.JP, err) - continue - } - if !slices.Equal(tc.Expected, matched) { - t.Errorf("'%s'->'%s' = %v (expected %v)", tc.J, tc.JP, matched, tc.Expected) - } } } @@ -137,35 +141,35 @@ func TestTransformations(t *testing.T) { const Path1 = `$[1].b[0]` const Path2 = `$[2][2]` - cases := []struct { + cases := map[string]struct { T Transformation Document string Paths []string Values []string Expected string }{ - { + "set operation": { T: Set, Document: Document1, Paths: []string{Path1, Path2}, Values: []string{"1", "2"}, Expected: `["a", {"b": [1, false]}, [10, 20, 2]]`, }, - { + "insert operation": { T: Insert, Document: Document1, Paths: []string{Path1, Path2}, Values: []string{"1", "2"}, Expected: `["a", {"b": [true, false]}, [10, 20, 2]]`, }, - { + "replace operation": { T: Replace, Document: Document1, Paths: []string{Path1, Path2}, Values: []string{"1", "2"}, Expected: `["a", {"b": [1, false]}, [10, 20]]`, }, - { + "remove operation": { T: Remove, Document: Document1, Paths: []string{`$[2]`, `$[1].b[1]`, `$[1].b[1]`}, @@ -173,27 +177,24 @@ func TestTransformations(t *testing.T) { }, } - for _, tc := range cases { - doc := json(t, tc.Document) - - var paths []*Path - for _, p := range tc.Paths { - paths = append(paths, path(t, p)) - } - - var values []*Value - for _, v := range tc.Values { - values = append(values, json(t, v)) - } - - err := ApplyTransform(tc.T, doc, paths, values) - if err != nil { - t.Fatal(err) - } - - result := string(doc.MarshalTo(nil)) - if result != tc.Expected { - t.Errorf("bad transformation (%v)\nwant: %s\ngot: %s", tc.T, tc.Expected, result) - } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + doc := json(t, tc.Document) + + var paths []*Path + for _, p := range tc.Paths { + paths = append(paths, path(t, p)) + } + + var values []*Value + for _, v := range tc.Values { + values = append(values, json(t, v)) + } + + err := ApplyTransform(tc.T, doc, paths, values) + require.NoError(t, err) + + require.Equal(t, tc.Expected, string(doc.MarshalTo(nil))) + }) } }