github.com/dolthub/go-mysql-server@v0.18.0/sql/types/json_test.go (about) 1 // Copyright 2022 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package types 16 17 import ( 18 "fmt" 19 "reflect" 20 "testing" 21 22 "github.com/dolthub/go-mysql-server/sql" 23 // _ "github.com/dolthub/go-mysql-server/sql/variables" 24 "github.com/dolthub/vitess/go/vt/proto/query" 25 "github.com/stretchr/testify/assert" 26 "github.com/stretchr/testify/require" 27 ) 28 29 func TestJsonCompare(t *testing.T) { 30 tests := []struct { 31 left string 32 right string 33 cmp int 34 }{ 35 // type precedence hierarchy: BOOLEAN, ARRAY, OBJECT, STRING, DOUBLE, NULL 36 {`true`, `[0]`, 1}, 37 {`[0]`, `{"a": 0}`, 1}, 38 {`{"a": 0}`, `"a"`, 1}, 39 {`"a"`, `0`, 1}, 40 {`0`, `null`, 1}, 41 42 // json null 43 {`null`, `0`, -1}, 44 {`0`, `null`, 1}, 45 {`null`, `null`, 0}, 46 47 // boolean 48 {`true`, `false`, 1}, 49 {`true`, `true`, 0}, 50 {`false`, `false`, 0}, 51 52 // strings 53 {`"A"`, `"B"`, -1}, 54 {`"A"`, `"A"`, 0}, 55 {`"C"`, `"B"`, 1}, 56 57 // numbers 58 {`0`, `0.0`, 0}, 59 {`0`, `-1`, 1}, 60 {`0`, `3.14`, -1}, 61 62 // arrays 63 {`[1,2]`, `[1,2]`, 0}, 64 {`[1,9]`, `[1,2]`, 1}, 65 {`[1,2]`, `[1,2,3]`, -1}, 66 67 // objects 68 {`{"a": 0}`, `{"a": 0}`, 0}, 69 // deterministic object ordering with arbitrary rules 70 {`{"a": 1}`, `{"a": 0}`, 1}, // 1 > 0 71 {`{"a": 0}`, `{"a": 0, "b": 1}`, -1}, // longer 72 {`{"a": 0, "c": 2}`, `{"a": 0, "b": 1}`, 1}, // "c" > "b" 73 74 // nested 75 { 76 left: `{"one": ["x", "y", "z"], "two": { "a": 0, "b": 1}, "three": false, "four": null, "five": " "}`, 77 right: `{"one": ["x", "y", "z"], "two": { "a": 0, "b": 1}, "three": false, "four": null, "five": " "}`, 78 cmp: 0, 79 }, 80 { 81 left: `{"one": ["x", "y"], "two": { "a": 0, "b": 1}, "three": false, "four": null, "five": " "}`, 82 right: `{"one": ["x", "y", "z"], "two": { "a": 0, "b": 1}, "three": false, "four": null, "five": " "}`, 83 cmp: -1, 84 }, 85 } 86 87 for _, test := range tests { 88 name := fmt.Sprintf("%v_%v__%d", test.left, test.right, test.cmp) 89 t.Run(name, func(t *testing.T) { 90 cmp, err := JSON.Compare( 91 MustJSON(test.left), 92 MustJSON(test.right), 93 ) 94 require.NoError(t, err) 95 assert.Equal(t, test.cmp, cmp) 96 }) 97 } 98 } 99 100 func TestJsonCompareNulls(t *testing.T) { 101 tests := []struct { 102 left interface{} 103 right interface{} 104 cmp int 105 }{ 106 {nil, MustJSON(`{"key": "value"}`), 1}, 107 {MustJSON(`{"key": "value"}`), nil, -1}, 108 {nil, nil, 0}, 109 {nil, MustJSON(`null`), 1}, 110 {MustJSON(`null`), nil, -1}, 111 } 112 113 for _, test := range tests { 114 name := fmt.Sprintf("%v_%v__%d", test.left, test.right, test.cmp) 115 t.Run(name, func(t *testing.T) { 116 cmp, err := JSON.Compare(test.left, test.right) 117 require.NoError(t, err) 118 assert.Equal(t, test.cmp, cmp) 119 }) 120 } 121 } 122 123 func TestJsonConvert(t *testing.T) { 124 type testStruct struct { 125 Field string `json:"field"` 126 } 127 tests := []struct { 128 val interface{} 129 expectedVal interface{} 130 expectedErr bool 131 }{ 132 {`""`, MustJSON(`""`), false}, 133 {[]int{1, 2}, MustJSON(`[1, 2]`), false}, 134 {`{"a": true, "b": 3}`, MustJSON(`{"a":true,"b":3}`), false}, 135 {[]byte(`{"a": true, "b": 3}`), MustJSON(`{"a":true,"b":3}`), false}, 136 {testStruct{Field: "test"}, MustJSON(`{"field":"test"}`), false}, 137 {MustJSON(`{"field":"test"}`), MustJSON(`{"field":"test"}`), false}, 138 {[]string{}, MustJSON(`[]`), false}, 139 {[]string{`555-555-5555`}, MustJSON(`["555-555-5555"]`), false}, 140 } 141 142 for _, test := range tests { 143 t.Run(fmt.Sprintf("%v %v", test.val, test.expectedVal), func(t *testing.T) { 144 val, _, err := JSON.Convert(test.val) 145 if test.expectedErr { 146 assert.Error(t, err) 147 } else { 148 require.NoError(t, err) 149 assert.Equal(t, test.expectedVal, val) 150 if val != nil { 151 assert.True(t, reflect.TypeOf(val).Implements(JSON.ValueType())) 152 } 153 } 154 }) 155 } 156 } 157 158 func TestJsonString(t *testing.T) { 159 require.Equal(t, "json", JSON.String()) 160 } 161 162 func TestJsonSQL(t *testing.T) { 163 tests := []struct { 164 val interface{} 165 expectedErr bool 166 }{ 167 {`""`, false}, 168 {`"555-555-555"`, false}, 169 {`{}`, false}, 170 {`{"field":"test"}`, false}, 171 {MustJSON(`{"field":"test"}`), false}, 172 {"1", false}, 173 {`[1,2,3]`, false}, 174 {[]int{1, 2, 3}, false}, 175 {[]string{"1", "2", "3"}, false}, 176 {"thisisbad", true}, 177 } 178 179 for _, test := range tests { 180 t.Run(fmt.Sprintf("%v", test.val), func(t *testing.T) { 181 val, err := JSON.SQL(sql.NewEmptyContext(), nil, test.val) 182 if test.expectedErr { 183 require.Error(t, err) 184 } else { 185 require.NoError(t, err) 186 assert.Equal(t, query.Type_JSON, val.Type()) 187 } 188 }) 189 } 190 191 // test that nulls are null 192 t.Run(fmt.Sprintf("%v", nil), func(t *testing.T) { 193 val, err := JSON.SQL(sql.NewEmptyContext(), nil, nil) 194 require.NoError(t, err) 195 assert.Equal(t, query.Type_NULL_TYPE, val.Type()) 196 }) 197 } 198 199 func TestValuer(t *testing.T) { 200 var empty JSONDocument 201 res, err := empty.Value() 202 require.NoError(t, err) 203 require.Equal(t, nil, res) 204 205 withVal := JSONDocument{ 206 Val: map[string]string{ 207 "a": "one", 208 }, 209 } 210 res, err = withVal.Value() 211 require.NoError(t, err) 212 require.Equal(t, `{"a": "one"}`, res) 213 } 214 215 type JsonRoundtripTest struct { 216 desc string 217 input string 218 expected string 219 } 220 221 var JsonRoundtripTests = []JsonRoundtripTest{ 222 { 223 desc: "formatted json", 224 input: `{"a": 1, "b": 2}`, 225 expected: `{"a": 1, "b": 2}`, 226 }, 227 { 228 desc: "unordered keys with correct spacing", 229 input: `{"b": 2, "a": 1}`, 230 expected: `{"a": 1, "b": 2}`, 231 }, 232 { 233 desc: "missing spaces after comma and colon", 234 input: `{"a":1,"b":2}`, 235 expected: `{"a": 1, "b": 2}`, 236 }, 237 { 238 desc: "unordered keys with no spacing", 239 input: `{"b":2,"a":1}`, 240 expected: `{"a": 1, "b": 2}`, 241 }, 242 { 243 desc: "unordered keys with extra spaces", 244 input: `{"b" : 2, "a" : 1}`, 245 expected: `{"a": 1, "b": 2}`, 246 }, 247 { 248 desc: "unordered keys with extra spaces and missing spaces after comma and colon", 249 input: `{"b" :2,"a" :1}`, 250 expected: `{"a": 1, "b": 2}`, 251 }, 252 { 253 desc: "arrays of primitives without spaces", 254 input: `{"a":[1,2,3],"b":[4,5,6]}`, 255 expected: `{"a": [1, 2, 3], "b": [4, 5, 6]}`, 256 }, 257 { 258 desc: "unordered keys with arrays of primitives", 259 input: `{"b":[4,5,6],"a":[1,2,3]}`, 260 expected: `{"a": [1, 2, 3], "b": [4, 5, 6]}`, 261 }, 262 { 263 desc: "arrays of objects without spaces", 264 input: `{"a":[{"a":1},{"b":2}],"b":[{"c":3},{"d":4}]}`, 265 expected: `{"a": [{"a": 1}, {"b": 2}], "b": [{"c": 3}, {"d": 4}]}`, 266 }, 267 { 268 desc: "unordered keys with arrays of objects", 269 input: `{"b":[{"c":3},{"d":4}],"a":[{"a":1},{"b":2}]}`, 270 expected: `{"a": [{"a": 1}, {"b": 2}], "b": [{"c": 3}, {"d": 4}]}`, 271 }, 272 { 273 desc: "unordered keys with arrays of objects with extra spaces", 274 input: `{"b" : [ { "c" : 3 }, { "d" : 4 } ], "a" : [ { "a" : 1 }, { "b" : 2 } ] }`, 275 expected: `{"a": [{"a": 1}, {"b": 2}], "b": [{"c": 3}, {"d": 4}]}`, 276 }, 277 { 278 desc: "arrays of objects with extra spaces", 279 input: `{"a": [ { "a" : 1 }, { "b" : 2 } ], "b": [ { "c" : 3 }, { "d" : 4 } ] }`, 280 expected: `{"a": [{"a": 1}, {"b": 2}], "b": [{"c": 3}, {"d": 4}]}`, 281 }, 282 } 283 284 func TestJsonRoundTripping(t *testing.T) { 285 for _, test := range JsonRoundtripTests { 286 t.Run("JSON roundtripping: "+test.desc, func(t *testing.T) { 287 val, err := JSON.SQL(sql.NewEmptyContext(), nil, test.input) 288 require.NoError(t, err) 289 assert.Equal(t, test.expected, val.ToString()) 290 }) 291 } 292 } 293 294 type JsonMutationTest struct { 295 desc string 296 doc string 297 path string 298 value string 299 resultVal string 300 changed bool 301 // expectErrStr string 302 } 303 304 var JsonSetTests = []JsonMutationTest{ 305 { 306 desc: "set root", 307 doc: `{"a": 1, "b": 2}`, 308 path: "$", 309 value: `{"c": 3}`, 310 resultVal: `{"c": 3}`, 311 changed: true, 312 }, 313 { 314 desc: "set root ignore white space", 315 doc: `{"a": 1, "b": 2}`, 316 path: " $ ", 317 value: `{"c": 3}`, 318 resultVal: `{"c": 3}`, 319 changed: true, 320 }, 321 { 322 desc: "set middle of an array", 323 doc: `[1, 2, 3]`, 324 path: "$[1]", 325 value: `42`, 326 resultVal: `[1, 42, 3]`, 327 changed: true, 328 }, 329 { 330 desc: "set last item of an array", 331 doc: `[1, 2, 3]`, 332 path: "$[2]", 333 value: `42`, 334 resultVal: `[1, 2, 42]`, 335 changed: true, 336 }, 337 { 338 desc: "append to an array when overflown", 339 doc: `[1, 2, 3]`, 340 path: "$[23]", 341 value: `42`, 342 resultVal: `[1, 2, 3, 42]`, 343 changed: true, 344 }, 345 { 346 desc: "set 'last' element of an array", 347 doc: `[1, 2, 3]`, 348 path: "$[last]", 349 value: `42`, 350 resultVal: `[1, 2, 42]`, 351 changed: true, 352 }, 353 { 354 desc: "set 'last-0' element of an array", 355 doc: `[1, 2, 3]`, 356 path: "$[last-0]", 357 value: `42`, 358 resultVal: `[1, 2, 42]`, 359 changed: true, 360 }, 361 { 362 desc: "set 'last-23' element of an array", 363 doc: `[1, 2, 3]`, 364 path: "$[last-23]", 365 value: `42`, 366 resultVal: `[42, 2, 3]`, 367 changed: true, 368 }, 369 { 370 desc: "array index ignores white space", 371 doc: `[1, 2, 3]`, 372 path: "$[ last - 1]", 373 value: `42`, 374 resultVal: `[1, 42, 3]`, 375 changed: true, 376 }, 377 { 378 desc: "empty array last index is 0", 379 doc: `[]`, 380 path: "$[last]", 381 value: `42`, 382 resultVal: `[42]`, 383 changed: true, 384 }, 385 { 386 desc: "treating object as an array replaces for index 0", 387 doc: `{"a":1}`, 388 path: "$[0]", 389 value: `42`, 390 resultVal: `42`, 391 changed: true, 392 }, 393 { 394 desc: "treating object as an array replaces for index last", 395 doc: `{"a":1}`, 396 path: "$[last]", 397 value: `42`, 398 resultVal: `42`, 399 changed: true, 400 }, 401 { 402 desc: "treating object will prefix as an array", 403 doc: `{"a":1}`, 404 path: "$[last-23]", 405 value: `42`, 406 resultVal: `[42, {"a": 1}]`, 407 changed: true, 408 }, 409 { 410 desc: "treating object will append as an array for out of bounds", 411 doc: `{"a":1}`, 412 path: "$[51]", 413 value: `42`, 414 resultVal: `[{"a": 1}, 42]`, 415 changed: true, 416 }, 417 { 418 desc: "scalar will append as an array for out of bounds", 419 doc: `17`, 420 path: "$[51]", 421 value: `42`, 422 resultVal: `[17, 42]`, 423 changed: true, 424 }, 425 { 426 desc: "scalar will be overwritten for index 0", 427 doc: `17`, 428 path: "$[0]", 429 value: `42`, 430 resultVal: `42`, 431 changed: true, 432 }, 433 { 434 desc: "scalar will be prefixed when underflow happens", 435 doc: `17`, 436 path: "$[last-23]", 437 value: `42`, 438 resultVal: `[42, 17]`, 439 changed: true, 440 }, 441 { 442 desc: "Object field updated", 443 doc: `{"a": 1}`, 444 path: "$.a", 445 value: `42`, 446 resultVal: `{"a": 42}`, 447 changed: true, 448 }, 449 { 450 desc: "Object field set", 451 doc: `{"a": 1}`, 452 path: "$.b", 453 value: `42`, 454 resultVal: `{"a": 1, "b": 42}`, 455 changed: true, 456 }, 457 { 458 desc: "Object field set Unicode", 459 doc: `{"โค๏ธ๐งก๐๐๐๐": {}}`, 460 path: `$."โค๏ธ๐งก๐๐๐๐"`, 461 value: `42`, 462 resultVal: `{"โค๏ธ๐งก๐๐๐๐": 42 }`, 463 changed: true, 464 }, 465 { 466 desc: "Object field name can optionally have quotes", 467 doc: `{"a": {}}`, 468 path: `$."a"`, 469 value: `42`, 470 resultVal: `{"a": 42 }`, 471 changed: true, 472 }, 473 { 474 desc: "object field name can contain escaped quotes", 475 doc: `{"\"a\"": {}}`, 476 path: `$."\"a\""`, 477 value: `42`, 478 resultVal: `{"\"a\"": 42 }`, 479 changed: true, 480 }, 481 { 482 desc: "Object field can be set to null", 483 doc: `{"a": {}}`, 484 path: `$."a"`, 485 value: `null`, 486 resultVal: `{"a": null }`, 487 changed: true, 488 }, 489 { 490 desc: "Array treated as an object is a no op", 491 doc: `[1, 2, 3]`, 492 path: `$.a`, 493 value: `42`, 494 resultVal: `[1, 2, 3]`, 495 changed: false, 496 }, 497 { 498 desc: "Setting in a nested array works", 499 doc: `[1, [2]]`, 500 path: "$[1][0]", 501 changed: true, 502 value: `42`, 503 resultVal: `[1, [42]]`, 504 }, 505 { 506 desc: "Setting in a nested objects works", 507 doc: `{"a": {"b": 1}}`, 508 path: "$.a.b", 509 changed: true, 510 value: `42`, 511 resultVal: `{"a": {"b": 42}}`, 512 }, 513 { 514 desc: "Setting in a nested several levels deep works", 515 doc: `{"a": {"b": [1,2,3,4,[5,6,7]]}}`, 516 path: "$.a.b[4][1]", 517 changed: true, 518 value: `96`, 519 resultVal: `{"a": {"b": [1,2,3,4,[5,96,7]]}}`, 520 }, 521 { 522 desc: "setting in a nested several levels deep works", 523 doc: `[9,8, {"a": [3,4,5] } ]`, 524 path: "$[2].a[0]", 525 changed: true, 526 value: `96`, 527 resultVal: `[9,8, {"a": [96,4,5]}]`, 528 }, 529 { 530 desc: "setting a deep path has no effect", 531 doc: `{}`, 532 path: "$.a.b.c", 533 changed: false, 534 value: `42`, 535 resultVal: `{}`, 536 }, 537 { 538 desc: "setting a deep path has no effect", 539 doc: `{}`, 540 path: "$.a.b[last]", 541 changed: false, 542 value: `42`, 543 resultVal: `{}`, 544 }, 545 { 546 // I've verified that null in MySQL is treated as a scalar in all ways that I can tell, which makes sense. 547 // Therefore, testing beyond these two tests doesn't necessary. 548 desc: "setting a null doc with a value results in the value", 549 doc: `null`, 550 path: "$", 551 changed: true, 552 value: `{"a": 42}`, 553 resultVal: `{"a": 42}`, 554 }, 555 { 556 desc: "setting a null doc with a value results in the value", 557 doc: `{"a" : 1}`, 558 path: "$", 559 changed: true, 560 value: `null`, 561 resultVal: `null`, 562 }, 563 564 /* Known ways we don't behave like MySQL. Frankly if anyone is depending on these behaviors they are doing it wrong. 565 mysql> select JSON_SET('{"a": 1}', "$[0][0]", 42); 566 +-------------------------------------+ 567 | JSON_SET('{"a": 1}', "$[0][0]", 42) | 568 +-------------------------------------+ 569 | 42 | 570 +-------------------------------------+ 571 572 mysql> select JSON_SET('{"a": 1}', "$[0][1]", 42); 573 +-------------------------------------+ 574 | JSON_SET('{"a": 1}', "$[0][1]", 42) | 575 +-------------------------------------+ 576 | [{"a": 1}, 42] | 577 +-------------------------------------+ 578 579 mysql> select JSON_SET('{"a": 1}', "$[0][last-3]", 42); 580 +------------------------------------------+ 581 | JSON_SET('{"a": 1}', "$[0][last-3]", 42) | 582 +------------------------------------------+ 583 | [42, {"a": 1}] | 584 +------------------------------------------+ 585 The three examples above seems to indicate that MySQL coerces objects to arrays earlier in the search process than we do. 586 Reason for thinking this is that JSON_SET('{"a": 1}', "$[0][0][ANYTHING]", 42); is a no op. 587 */ 588 589 } 590 591 func TestJsonSet(t *testing.T) { 592 for _, test := range JsonSetTests { 593 t.Run("JSON set: "+test.desc, func(t *testing.T) { 594 doc := MustJSON(test.doc) 595 val := MustJSON(test.value) 596 res, changed, err := doc.Set(test.path, val) 597 require.NoError(t, err) 598 assert.Equal(t, MustJSON(test.resultVal), res) 599 assert.Equal(t, test.changed, changed) 600 }) 601 } 602 } 603 604 var JsonInsertTests = []JsonMutationTest{ 605 { 606 desc: "insert root", 607 doc: `{"a": 1, "b": 2}`, 608 path: "$", 609 value: `{"c": 3}`, 610 resultVal: `{"a": 1, "b": 2}`, 611 changed: false, 612 }, 613 614 { 615 desc: "insert root ignore white space", 616 doc: `{"a": 1, "b": 2}`, 617 path: " $ ", 618 value: `{"c": 3}`, 619 resultVal: `{"a": 1, "b": 2}`, 620 changed: false, 621 }, 622 { 623 desc: "insert middle of an array", 624 doc: `[1, 2, 3]`, 625 path: "$[1]", 626 value: `42`, 627 resultVal: `[1, 2, 3]`, 628 changed: false, 629 }, 630 { 631 desc: "insert last item of an array does nothing", 632 doc: `[1, 2, 3]`, 633 path: "$[2]", 634 value: `42`, 635 resultVal: `[1, 2, 3]`, 636 changed: false, 637 }, 638 { 639 desc: "append to an array when overflown", 640 doc: `[1, 2, 3]`, 641 path: "$[23]", 642 value: `42`, 643 resultVal: `[1, 2, 3, 42]`, 644 changed: true, 645 }, 646 { 647 desc: "insert 'last' element of an array does nothing", 648 doc: `[1, 2, 3]`, 649 path: "$[last]", 650 value: `42`, 651 resultVal: `[1, 2, 3]`, 652 changed: false, 653 }, 654 { 655 desc: "insert into empty array mutates", 656 doc: `[]`, 657 path: "$[last]", 658 value: `42`, 659 resultVal: `[42]`, 660 changed: true, 661 }, 662 { 663 desc: "treating object as an array replaces for index 0", 664 doc: `{"a":1}`, 665 path: "$[0]", 666 value: `42`, 667 resultVal: `{"a":1}`, 668 changed: false, 669 }, 670 { 671 // Can't make this stuff up. 672 // mysql> select JSON_INSERT(JSON_OBJECT("a",1),'$[last-21]', 42); 673 // +--------------------------------------------------+ 674 // | JSON_INSERT(JSON_OBJECT("a",1),'$[last-21]', 42) | 675 // +--------------------------------------------------+ 676 // | [42, {"a": 1}] | 677 // +--------------------------------------------------+ 678 desc: "treating object will prefix as an array", 679 doc: `{"a":1}`, 680 path: "$[last-23]", 681 value: `42`, 682 resultVal: `[42, {"a": 1}]`, 683 changed: true, 684 }, 685 { 686 // mysql> select JSON_INSERT(JSON_OBJECT("a",1),'$[51]', 42); 687 // +---------------------------------------------+ 688 // | JSON_INSERT(JSON_OBJECT("a",1),'$[51]', 42) | 689 // +---------------------------------------------+ 690 // | [{"a": 1}, 42] | 691 // +---------------------------------------------+ 692 desc: "treating object will append as an array for out of bounds", 693 doc: `{"a":1}`, 694 path: "$[51]", 695 value: `42`, 696 resultVal: `[{"a": 1}, 42]`, 697 changed: true, 698 }, 699 { 700 desc: "scalar will append as an array for out of bounds", 701 doc: `17`, 702 path: "$[51]", 703 value: `42`, 704 resultVal: `[17, 42]`, 705 changed: true, 706 }, 707 { 708 desc: "scalar will not be overwritten for index 0", 709 doc: `17`, 710 path: "$[0]", 711 value: `42`, 712 resultVal: `17`, 713 changed: false, 714 }, 715 { 716 desc: "scalar will be prefixed when underflow happens", 717 doc: `17`, 718 path: "$[last-23]", 719 value: `42`, 720 resultVal: `[42, 17]`, 721 changed: true, 722 }, 723 { 724 desc: "existing object field not updated", 725 doc: `{"a": 1}`, 726 path: "$.a", 727 value: `42`, 728 resultVal: `{"a": 1}`, 729 changed: false, 730 }, 731 { 732 desc: "new object field inserted", 733 doc: `{"a": 1}`, 734 path: "$.b", 735 value: `42`, 736 resultVal: `{"a": 1, "b": 42}`, 737 changed: true, 738 }, 739 { 740 desc: "object field name can optionally have quotes", 741 doc: `{"a": {}}`, 742 path: `$."a"`, 743 value: `42`, 744 resultVal: `{"a": {} }`, 745 changed: false, 746 }, 747 { 748 desc: "object field name can contain escaped quotes", 749 doc: `{"\"a\"": {}}`, 750 path: `$."\"a\""`, 751 value: `42`, 752 resultVal: `{"\"a\"": {} }`, 753 changed: false, 754 }, 755 { 756 desc: "array treated as an object is a no op", 757 doc: `[1, 2, 3]`, 758 path: `$.a`, 759 value: `42`, 760 resultVal: `[1, 2, 3]`, 761 changed: false, 762 }, 763 764 { 765 desc: "inserting a deep path has no effect", 766 doc: `{}`, 767 path: "$.a.b.c", 768 changed: false, 769 value: `42`, 770 resultVal: `{}`, 771 }, 772 { 773 desc: "inserting a deep path has no effect", 774 doc: `{}`, 775 path: "$.a.b[last]", 776 changed: false, 777 value: `42`, 778 resultVal: `{}`, 779 }, 780 } 781 782 func TestJsonInsert(t *testing.T) { 783 for _, test := range JsonInsertTests { 784 t.Run("JSON insert: "+test.desc, func(t *testing.T) { 785 doc := MustJSON(test.doc) 786 val := MustJSON(test.value) 787 res, changed, err := doc.Insert(test.path, val) 788 require.NoError(t, err) 789 assert.Equal(t, MustJSON(test.resultVal), res) 790 assert.Equal(t, test.changed, changed) 791 }) 792 } 793 } 794 795 var JsonRemoveTests = []JsonMutationTest{ 796 { 797 desc: "remove middle of an array", 798 doc: `[1, 2, 3]`, 799 path: "$[1]", 800 resultVal: `[1, 3]`, 801 changed: true, 802 }, 803 { 804 desc: "remove last item of an array", 805 doc: `[1, 2, 3]`, 806 path: "$[2]", 807 resultVal: `[1, 2]`, 808 changed: true, 809 }, 810 { 811 desc: "no op remove on array when overflown", 812 doc: `[1, 2, 3]`, 813 path: "$[23]", 814 resultVal: `[1, 2, 3]`, 815 changed: false, 816 }, 817 { 818 desc: "remove 'last' element of an array", 819 doc: `[1, 2, 3]`, 820 path: "$[last]", 821 resultVal: `[1, 2]`, 822 changed: true, 823 }, 824 { 825 desc: "remove 'last-0' element of an array", 826 doc: `[1, 2, 3]`, 827 path: "$[last-0]", 828 resultVal: `[1, 2]`, 829 changed: true, 830 }, 831 { 832 desc: "no op remove on underflow 'last-23' element of an array", 833 doc: `[1, 2, 3]`, 834 path: "$[last-23]", 835 resultVal: `[1, 2, 3]`, 836 changed: false, 837 }, 838 { 839 desc: "no op remove with empty array", 840 doc: `[]`, 841 path: "$[last]", 842 resultVal: `[]`, 843 changed: false, 844 }, 845 { 846 // Remove behaves much more reasonably than other operations when the path is invalid. When you treat an 847 // object or scalar as an array, it is a no-op. period. For this reason, there are far fewer remove tests than 848 // there are for insert/set/replace. 849 desc: "treating object as an array is no op", 850 doc: `{"a":1}`, 851 path: "$[0]", 852 resultVal: `{"a" : 1}`, 853 changed: false, 854 }, 855 { 856 desc: "scalar will append as an array for out of bounds", 857 doc: `17`, 858 path: "$[0]", 859 resultVal: `17`, 860 changed: false, 861 }, 862 { 863 desc: "Object field updated", 864 doc: `{"a": 1, "b": 2}`, 865 path: "$.a", 866 resultVal: `{"b": 2}`, 867 changed: true, 868 }, 869 { 870 desc: "No op object change when field not found", 871 doc: `{"a": 1}`, 872 path: "$.b", 873 resultVal: `{"a": 1}`, 874 changed: false, 875 }, 876 } 877 878 func TestJsonRemove(t *testing.T) { 879 for _, test := range JsonRemoveTests { 880 t.Run("JSON remove: "+test.desc, func(t *testing.T) { 881 doc := MustJSON(test.doc) 882 res, changed, err := doc.Remove(test.path) 883 require.NoError(t, err) 884 assert.Equal(t, MustJSON(test.resultVal), res) 885 assert.Equal(t, test.changed, changed) 886 }) 887 } 888 } 889 890 var JsonReplaceTests = []JsonMutationTest{ 891 { 892 desc: "replace root", 893 doc: `{"a": 1, "b": 2}`, 894 path: "$", 895 value: `{"c": 3}`, 896 resultVal: `{"c": 3}`, 897 changed: true, 898 }, 899 900 { 901 desc: "replace root ignore white space", 902 doc: `{"a": 1, "b": 2}`, 903 path: " $ ", 904 value: `{"c": 3}`, 905 resultVal: `{"c": 3}`, 906 changed: true, 907 }, 908 { 909 desc: "replace middle of an array", 910 doc: `[1, 2, 3]`, 911 path: "$[1]", 912 value: `42`, 913 resultVal: `[1, 42, 3]`, 914 changed: true, 915 }, 916 { 917 desc: "set last item of an array", 918 doc: `[1, 2, 3]`, 919 path: "$[2]", 920 value: `42`, 921 resultVal: `[1, 2, 42]`, 922 changed: true, 923 }, 924 { 925 desc: "noupdate to an array when overflown", 926 doc: `[1, 2, 3]`, 927 path: "$[23]", 928 value: `42`, 929 resultVal: `[1, 2, 3]`, 930 changed: false, 931 }, 932 { 933 desc: "replace 'last' element of an array", 934 doc: `[1, 2, 3]`, 935 path: "$[last]", 936 value: `42`, 937 resultVal: `[1, 2, 42]`, 938 changed: true, 939 }, 940 { 941 // mysql> select JSON_REPLACE(JSON_ARRAY(1,2,3),'$[last-23]', 42); 942 // +--------------------------------------------------+ 943 // | JSON_REPLACE(JSON_ARRAY(1,2,3),'$[last-23]', 42) | 944 // +--------------------------------------------------+ 945 // | [1, 2, 3] | 946 // +--------------------------------------------------+ 947 // 1 row in set (0.00 sec) 948 desc: "no update for element underflow", 949 doc: `[1, 2, 3]`, 950 path: "$[last-23]", 951 value: `42`, 952 resultVal: `[1, 2, 3]`, 953 changed: false, 954 }, 955 { 956 desc: "no update for empty array", 957 doc: `[]`, 958 path: "$[0]", 959 value: `42`, 960 resultVal: `[]`, 961 changed: false, 962 }, 963 { 964 desc: "treating object as an array replaces for index 0", 965 doc: `{"a":1}`, 966 path: "$[0]", 967 value: `42`, 968 resultVal: `42`, 969 changed: true, 970 }, 971 { 972 // mysql> select JSON_REPLACE(JSON_OBJECT("a",1),'$[last]', 42); 973 // +------------------------------------------------+ 974 // | JSON_REPLACE(JSON_OBJECT("a",1),'$[last]', 42) | 975 // +------------------------------------------------+ 976 // | 42 | 977 // +------------------------------------------------+ 978 desc: "treating object as an array replaces for index last", 979 doc: `{"a":1}`, 980 path: "$[last]", 981 value: `42`, 982 resultVal: `42`, 983 changed: true, 984 }, 985 { 986 desc: "no op when treating object as an array with underflow", 987 doc: `{"a":1}`, 988 path: "$[last-23]", 989 value: `42`, 990 resultVal: `{"a": 1}`, 991 changed: false, 992 }, 993 { 994 desc: "no op when treating object as an array with overflow", 995 doc: `{"a":1}`, 996 path: "$[51]", 997 value: `42`, 998 resultVal: `{"a": 1}`, 999 changed: false, 1000 }, 1001 { 1002 desc: "no update for scalar will treated as an array for out of bounds", 1003 doc: `17`, 1004 path: "$[51]", 1005 value: `42`, 1006 resultVal: `17`, 1007 changed: false, 1008 }, 1009 { 1010 desc: "scalar will be overwritten for index 0", 1011 doc: `17`, 1012 path: "$[0]", 1013 value: `42`, 1014 resultVal: `42`, 1015 changed: true, 1016 }, 1017 { 1018 desc: "no update for scalar when used as an array with underflow", 1019 doc: `17`, 1020 path: "$[last-23]", 1021 value: `42`, 1022 resultVal: `17`, 1023 changed: false, 1024 }, 1025 { 1026 desc: "Object field updated", 1027 doc: `{"a": 1}`, 1028 path: "$.a", 1029 value: `42`, 1030 resultVal: `{"a": 42}`, 1031 changed: true, 1032 }, 1033 { 1034 desc: "Object field not inserted", 1035 doc: `{"a": 1}`, 1036 path: "$.b", 1037 value: `42`, 1038 resultVal: `{"a": 1}`, 1039 changed: false, 1040 }, 1041 } 1042 1043 func TestJsonReplace(t *testing.T) { 1044 for _, test := range JsonReplaceTests { 1045 t.Run("JSON replace: "+test.desc, func(t *testing.T) { 1046 doc := MustJSON(test.doc) 1047 val := MustJSON(test.value) 1048 res, changed, err := doc.Replace(test.path, val) 1049 require.NoError(t, err) 1050 assert.Equal(t, MustJSON(test.resultVal), res) 1051 assert.Equal(t, test.changed, changed) 1052 }) 1053 } 1054 } 1055 1056 var JsonArrayAppendTests = []JsonMutationTest{ 1057 { 1058 desc: "append to empty object", 1059 doc: `{}`, 1060 path: "$", 1061 value: `42`, 1062 changed: true, 1063 resultVal: `[{}, 42]`, 1064 }, 1065 { 1066 desc: "append to empty array", 1067 doc: `[]`, 1068 path: "$", 1069 value: `42`, 1070 changed: true, 1071 resultVal: `[42]`, 1072 }, 1073 { 1074 desc: "append to a nested array", 1075 doc: `[{"a": [1, 2, 3, 4]}]`, 1076 path: "$[0].a", 1077 value: `42`, 1078 changed: true, 1079 resultVal: `[{"a": [1, 2, 3, 4, 42]}]`, 1080 }, 1081 { 1082 desc: "append a scalar to a scalar leads to an array", 1083 doc: `{"a": "eh"}`, 1084 path: "$.a", 1085 changed: true, 1086 value: `42`, 1087 resultVal: `{"a": ["eh", 42]}`, 1088 }, 1089 { 1090 desc: "append to an array index", 1091 doc: `[[1, 2, 3], {"a": "eh"}]`, 1092 path: "$[0]", 1093 changed: true, 1094 value: `42`, 1095 resultVal: `[[1, 2, 3, 42], {"a": "eh"}]`, 1096 }, 1097 { 1098 desc: "append to an array index", 1099 doc: `[{"b" : "be"}, {"a": "eh"}]`, 1100 path: "$[0]", 1101 changed: true, 1102 value: `42`, 1103 resultVal: `[[{"b" : "be"}, 42], {"a": "eh"}]`, 1104 }, 1105 { 1106 desc: "no op when out of bounds", 1107 doc: `[1, 2, 3]`, 1108 path: "$[51]", 1109 changed: false, 1110 value: `42`, 1111 resultVal: `[1, 2, 3]`, 1112 }, 1113 { 1114 desc: "last index works for lookup", 1115 doc: `[1, 2, [3,4]]`, 1116 path: "$[last]", 1117 changed: true, 1118 value: `42`, 1119 resultVal: `[1, 2, [3, 4, 42]]`, 1120 }, 1121 { 1122 desc: "no op when there is an underflow", 1123 doc: `[1, 2, 3]`, 1124 path: "$[last-23]", 1125 changed: false, 1126 value: `42`, 1127 resultVal: `[1, 2, 3]`, 1128 }, 1129 } 1130 1131 func TestJsonArrayAppend(t *testing.T) { 1132 for _, test := range JsonArrayAppendTests { 1133 t.Run("JSON array append: "+test.desc, func(t *testing.T) { 1134 doc := MustJSON(test.doc) 1135 val := MustJSON(test.value) 1136 res, changed, err := doc.ArrayAppend(test.path, val) 1137 require.NoError(t, err) 1138 assert.Equal(t, MustJSON(test.resultVal), res) 1139 assert.Equal(t, test.changed, changed) 1140 }) 1141 } 1142 } 1143 1144 var JsonArrayInsertTests = []JsonMutationTest{ 1145 { 1146 desc: "array insert overflow appends", 1147 doc: `[1,2,3]`, 1148 path: "$[51]", 1149 value: `42`, 1150 resultVal: `[1,2,3,42]`, 1151 changed: true, 1152 }, 1153 { 1154 desc: "array insert at first element", 1155 doc: `[1,2,3]`, 1156 path: "$[0]", 1157 value: `42`, 1158 resultVal: `[42,1,2,3]`, 1159 changed: true, 1160 }, 1161 { 1162 desc: "array insert at second element", 1163 doc: `[1,2,3]`, 1164 path: "$[1]", 1165 value: `42`, 1166 resultVal: `[1,42,2,3]`, 1167 changed: true, 1168 }, 1169 { 1170 desc: "insert to empty array", 1171 doc: `{"a" :[]}`, 1172 path: "$.a[0]", 1173 value: `42`, 1174 resultVal: `{"a" : [42]}`, 1175 changed: true, 1176 }, 1177 { 1178 desc: "insert to an object no op", 1179 doc: `{"a" :{}}`, 1180 path: "$.a[0]", 1181 value: `42`, 1182 resultVal: `{"a" : {}}`, 1183 changed: false, 1184 }, 1185 // mysql> select json_array_insert(json_array(1,2,3), "$[last]", 42); 1186 // +-----------------------------------------------------+ 1187 // | json_array_insert(json_array(1,2,3), "$[last]", 42) | 1188 // +-----------------------------------------------------+ 1189 // | [1, 2, 42, 3] | 1190 // +-----------------------------------------------------+ 1191 { 1192 desc: "insert to [last] does crazy things", 1193 doc: `[1,2,3]`, 1194 path: "$[last]", 1195 value: `42`, 1196 resultVal: `[1,2,42,3]`, // It's true. Try it yourself. 1197 changed: true, 1198 }, 1199 { 1200 desc: "insert into non-array results in noop", 1201 doc: `{}`, 1202 path: "$[0]", 1203 value: `42`, 1204 changed: false, 1205 resultVal: `{}`, 1206 }, 1207 } 1208 1209 func TestJsonArrayInsert(t *testing.T) { 1210 for _, test := range JsonArrayInsertTests { 1211 t.Run("JSON array insert: "+test.desc, func(t *testing.T) { 1212 doc := MustJSON(test.doc) 1213 val := MustJSON(test.value) 1214 res, changed, err := doc.ArrayInsert(test.path, val) 1215 require.NoError(t, err) 1216 assert.Equal(t, MustJSON(test.resultVal), res) 1217 assert.Equal(t, test.changed, changed) 1218 }) 1219 } 1220 } 1221 1222 type parseErrTest struct { 1223 desc string 1224 doc string 1225 path string 1226 expectErrStr string 1227 } 1228 1229 var JsonPathParseErrTests = []parseErrTest{ 1230 { 1231 desc: "empty path", 1232 path: "", 1233 expectErrStr: "Invalid JSON path expression. Empty path", 1234 }, 1235 { 1236 desc: "non $ prefix", 1237 path: "bogus", 1238 expectErrStr: "Invalid JSON path expression. Path must start with '$'", 1239 }, 1240 { 1241 desc: "dot to nowhere", 1242 path: "$.", 1243 expectErrStr: "Invalid JSON path expression. Expected field name after '.' at character 2 of $.", 1244 }, 1245 { 1246 desc: "no . or [", 1247 path: "$fu.bar", 1248 expectErrStr: "Invalid JSON path expression. Expected '.' or '[' at character 1 of $fu.bar", 1249 }, 1250 { 1251 desc: "incomplete quoted field", 1252 path: `$."a"."b`, 1253 expectErrStr: `Invalid JSON path expression. '"' expected at character 6 of $."a"."b`, 1254 }, 1255 { 1256 desc: "invalid bare string", 1257 path: "$.a@<>bc", 1258 expectErrStr: `Invalid JSON path expression. Expected '.' or '[' at character 3 of $.a@<>bc`, 1259 }, 1260 { 1261 desc: "non-integer array index", 1262 path: "$[abcd]", 1263 expectErrStr: `Invalid JSON path expression. Unable to convert abcd to an int at character 2 of $[abcd]`, 1264 }, 1265 { 1266 desc: "non-integer array index", 1267 path: "$[last-abcd]", 1268 expectErrStr: `Invalid JSON path expression. Expected a positive integer after 'last-' at character 6 of $[last-abcd]`, 1269 }, 1270 { 1271 desc: "too many dashes in last-", 1272 path: "$[last-abcd-xyz]", 1273 expectErrStr: `Invalid JSON path expression. Unable to convert last-abcd-xyz to an int at character 2 of $[last-abcd-xyz]`, 1274 }, 1275 } 1276 1277 func TestJsonPathErrors(t *testing.T) { 1278 doc := MustJSON(`{"a": {"b": 2} , "c": [1, 2, 3]}`) 1279 1280 for _, test := range JsonPathParseErrTests { 1281 t.Run("JSON Path: "+test.desc, func(t *testing.T) { 1282 _, changed, err := doc.Set(test.path, MustJSON(`{"a": 42}`)) 1283 assert.Equal(t, false, changed) 1284 require.Error(t, err) 1285 assert.Equal(t, test.expectErrStr, err.Error()) 1286 }) 1287 } 1288 } 1289 1290 var JsonArrayInsertErrors = []parseErrTest{ 1291 { 1292 desc: "empty path", 1293 path: "", 1294 expectErrStr: "Invalid JSON path expression. Empty path", 1295 }, 1296 { 1297 desc: "insert into root path results in an error", 1298 doc: `[]`, 1299 path: "$", 1300 expectErrStr: "Path expression is not a path to a cell in an array: $", 1301 }, 1302 { 1303 desc: "no op insert into non-array", 1304 doc: `{"a": "eh"}`, 1305 path: "$.a", 1306 expectErrStr: "A path expression is not a path to a cell in an array at character 3 of $.a", 1307 }, 1308 } 1309 1310 func TestJsonInsertErrors(t *testing.T) { 1311 doc := MustJSON(`{"a": {"b": 2} , "c": [1, 2, 3]}`) 1312 1313 for _, test := range JsonArrayInsertErrors { 1314 t.Run("JSON Path: "+test.desc, func(t *testing.T) { 1315 _, changed, err := doc.ArrayInsert(test.path, MustJSON(`{"a": 42}`)) 1316 assert.Equal(t, false, changed) 1317 require.Error(t, err) 1318 assert.Equal(t, test.expectErrStr, err.Error()) 1319 }) 1320 } 1321 } 1322 1323 func TestRemoveRoot(t *testing.T) { 1324 // Fairly special case situation which doesn't mesh with our other tests. MySQL returns a specfic message when you 1325 // attempt to remove the root document. 1326 doc := MustJSON(`{"a": 1, "b": 2}`) 1327 _, changed, err := doc.Remove("$") 1328 1329 require.Error(t, err) 1330 assert.Equal(t, "The path expression '$' is not allowed in this context.", err.Error()) 1331 assert.Equal(t, false, changed) 1332 } 1333 1334 type jsonIterKV struct { 1335 key string 1336 value interface{} 1337 } 1338 1339 type jsonIterTest struct { 1340 name string 1341 doc JsonObject 1342 expectedPairs []jsonIterKV 1343 } 1344 1345 var jsonIterTests = []jsonIterTest{ 1346 { 1347 name: "empty object", 1348 doc: JsonObject{}, 1349 expectedPairs: []jsonIterKV{}, 1350 }, 1351 { 1352 name: "iterate over keys in sorted order", 1353 doc: JsonObject{"b": 1, "a": 2}, 1354 expectedPairs: []jsonIterKV{ 1355 {key: "a", value: 2}, 1356 {key: "b", value: 1}, 1357 }, 1358 }, 1359 { 1360 name: "keys use lexicographic order, not key-length order", 1361 doc: JsonObject{"b": 1, "aa": 2}, 1362 expectedPairs: []jsonIterKV{ 1363 {key: "aa", value: 2}, 1364 {key: "b", value: 1}, 1365 }, 1366 }, 1367 } 1368 1369 func TestJsonIter(t *testing.T) { 1370 for _, test := range jsonIterTests { 1371 t.Run(test.name, func(t *testing.T) { 1372 iter := NewJSONIter(test.doc) 1373 pairs := make([]jsonIterKV, 0) 1374 for iter.HasNext() { 1375 var pair jsonIterKV 1376 var err error 1377 pair.key, pair.value, err = iter.Next() 1378 require.NoError(t, err) 1379 pairs = append(pairs, pair) 1380 } 1381 require.Equal(t, test.expectedPairs, pairs) 1382 }) 1383 } 1384 }