github.com/hamba/avro/v2@v2.22.1-0.20240518180522-aff3955acf7d/schema_compatibility_test.go (about) 1 package avro_test 2 3 import ( 4 "math/big" 5 "testing" 6 "time" 7 8 "github.com/hamba/avro/v2" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 ) 12 13 func TestNewSchemaCompatibility(t *testing.T) { 14 sc := avro.NewSchemaCompatibility() 15 16 assert.IsType(t, &avro.SchemaCompatibility{}, sc) 17 } 18 19 func TestSchemaCompatibility_Compatible(t *testing.T) { 20 tests := []struct { 21 name string 22 reader string 23 writer string 24 wantErr assert.ErrorAssertionFunc 25 }{ 26 { 27 name: "Primitive Matching", 28 reader: `"int"`, 29 writer: `"int"`, 30 wantErr: assert.NoError, 31 }, 32 { 33 name: "Int Promote Long", 34 reader: `"long"`, 35 writer: `"int"`, 36 wantErr: assert.NoError, 37 }, 38 { 39 name: "Int Promote Float", 40 reader: `"float"`, 41 writer: `"int"`, 42 wantErr: assert.NoError, 43 }, 44 { 45 name: "Int Promote Double", 46 reader: `"double"`, 47 writer: `"int"`, 48 wantErr: assert.NoError, 49 }, 50 { 51 name: "Long Promote Float", 52 reader: `"float"`, 53 writer: `"long"`, 54 wantErr: assert.NoError, 55 }, 56 { 57 name: "Long Promote Double", 58 reader: `"double"`, 59 writer: `"long"`, 60 wantErr: assert.NoError, 61 }, 62 { 63 name: "Float Promote Double", 64 reader: `"double"`, 65 writer: `"float"`, 66 wantErr: assert.NoError, 67 }, 68 { 69 name: "String Promote Bytes", 70 reader: `"bytes"`, 71 writer: `"string"`, 72 wantErr: assert.NoError, 73 }, 74 { 75 name: "Bytes Promote String", 76 reader: `"string"`, 77 writer: `"bytes"`, 78 wantErr: assert.NoError, 79 }, 80 { 81 name: "Union Match", 82 reader: `["int", "long", "string"]`, 83 writer: `["string", "int", "long"]`, 84 wantErr: assert.NoError, 85 }, 86 { 87 name: "Union Reader Missing Schema", 88 reader: `["int", "string"]`, 89 writer: `["string", "int", "long"]`, 90 wantErr: assert.Error, 91 }, 92 { 93 name: "Union Writer Missing Schema", 94 reader: `["int", "long", "string"]`, 95 writer: `["string", "int"]`, 96 wantErr: assert.NoError, 97 }, 98 { 99 name: "Union Writer Not Union", 100 reader: `["int", "long", "string"]`, 101 writer: `"int"`, 102 wantErr: assert.NoError, 103 }, 104 { 105 name: "Union Writer Not Union With Error", 106 reader: `["string"]`, 107 writer: `"int"`, 108 wantErr: assert.Error, 109 }, 110 { 111 name: "Union Reader Not Union", 112 reader: `"int"`, 113 writer: `["int"]`, 114 wantErr: assert.NoError, 115 }, 116 { 117 name: "Union Reader Not Union With Error", 118 reader: `"int"`, 119 writer: `["string", "int", "long"]`, 120 wantErr: assert.Error, 121 }, 122 { 123 name: "Array Match", 124 reader: `{"type":"array", "items": "int"}`, 125 writer: `{"type":"array", "items": "int"}`, 126 wantErr: assert.NoError, 127 }, 128 { 129 name: "Array Items Mismatch", 130 reader: `{"type":"array", "items": "int"}`, 131 writer: `{"type":"array", "items": "string"}`, 132 wantErr: assert.Error, 133 }, 134 { 135 name: "Map Match", 136 reader: `{"type":"map", "values": "int"}`, 137 writer: `{"type":"map", "values": "int"}`, 138 wantErr: assert.NoError, 139 }, 140 { 141 name: "Map Items Mismatch", 142 reader: `{"type":"map", "values": "int"}`, 143 writer: `{"type":"map", "values": "string"}`, 144 wantErr: assert.Error, 145 }, 146 { 147 name: "Fixed Match", 148 reader: `{"type":"fixed", "name":"test", "namespace": "org.hamba.avro", "size": 12}`, 149 writer: `{"type":"fixed", "name":"test", "namespace": "org.hamba.avro", "size": 12}`, 150 wantErr: assert.NoError, 151 }, 152 { 153 name: "Fixed Name Mismatch", 154 reader: `{"type":"fixed", "name":"test1", "namespace": "org.hamba.avro", "size": 12}`, 155 writer: `{"type":"fixed", "name":"test", "namespace": "org.hamba.avro", "size": 12}`, 156 wantErr: assert.Error, 157 }, 158 { 159 name: "Fixed Size Mismatch", 160 reader: `{"type":"fixed", "name":"test", "namespace": "org.hamba.avro", "size": 13}`, 161 writer: `{"type":"fixed", "name":"test", "namespace": "org.hamba.avro", "size": 12}`, 162 wantErr: assert.Error, 163 }, 164 { 165 name: "Enum Match", 166 reader: `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST1", "TEST2"]}`, 167 writer: `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST1", "TEST2"]}`, 168 wantErr: assert.NoError, 169 }, 170 { 171 name: "Enum Name Mismatch", 172 reader: `{"type":"enum", "name":"test1", "namespace": "org.hamba.avro", "symbols":["TEST1", "TEST2"]}`, 173 writer: `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST1", "TEST2"]}`, 174 wantErr: assert.Error, 175 }, 176 { 177 name: "Enum Reader Missing Symbol", 178 reader: `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST1"]}`, 179 writer: `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST1", "TEST2"]}`, 180 wantErr: assert.Error, 181 }, 182 { 183 name: "Enum Reader Missing Symbol With Default", 184 reader: `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST1"], "default": "TEST1"}`, 185 writer: `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST1", "TEST2"]}`, 186 wantErr: assert.NoError, 187 }, 188 { 189 name: "Enum Writer Missing Symbol", 190 reader: `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST1", "TEST2"]}`, 191 writer: `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST1"]}`, 192 wantErr: assert.NoError, 193 }, 194 { 195 name: "Record Match", 196 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}, {"name": "b", "type": "string"}]}`, 197 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "b", "type": "string"}, {"name": "a", "type": "int"}]}`, 198 wantErr: assert.NoError, 199 }, 200 { 201 name: "Record Name Mismatch", 202 reader: `{"type":"record", "name":"test1", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int", "default": 1}, {"name": "b", "type": "string"}]}`, 203 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "b", "type": "string", "default": "b"}, {"name": "a", "type": "int"}]}`, 204 wantErr: assert.Error, 205 }, 206 { 207 name: "Record Schema Mismatch", 208 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "string"}, {"name": "b", "type": "string"}]}`, 209 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "b", "type": "string"}, {"name": "a", "type": "int"}]}`, 210 wantErr: assert.Error, 211 }, 212 { 213 name: "Record Reader Field Missing", 214 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 215 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "b", "type": "string"}, {"name": "a", "type": "int"}]}`, 216 wantErr: assert.NoError, 217 }, 218 { 219 name: "Record Writer Field Missing With Default", 220 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}, {"name": "b", "type": "string", "default": "test"}]}`, 221 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 222 wantErr: assert.NoError, 223 }, 224 { 225 name: "Record Writer Field Missing Without Default", 226 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}, {"name": "b", "type": "string"}]}`, 227 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 228 wantErr: assert.Error, 229 }, 230 { 231 name: "Ref Dereference", 232 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": {"type":"record", "name":"test1", "namespace": "org.hamba.avro", "fields":[{"name": "b", "type": "int"}]}}, {"name": "b", "type": "test1"}]}`, 233 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": {"type":"record", "name":"test1", "namespace": "org.hamba.avro", "fields":[{"name": "b", "type": "int"}]}}, {"name": "b", "type": "test"}]}`, 234 wantErr: assert.Error, 235 }, 236 { 237 name: "Breaks Recursion", 238 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "test"}]}`, 239 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "test"}]}`, 240 wantErr: assert.NoError, 241 }, 242 { 243 name: "Comparison with different namespaces", 244 writer: `{"type":"record", "name":"Obj", "namespace": "ns", "fields":[{"name": "a", "type": "int"}]}`, 245 reader: `{"type":"record", "name":"Obj", "fields":[{"name": "a", "type": "int"}]}`, 246 wantErr: assert.NoError, 247 }, 248 } 249 250 for _, test := range tests { 251 test := test 252 t.Run(test.name, func(t *testing.T) { 253 t.Parallel() 254 255 r, err := avro.ParseWithCache(test.reader, "", &avro.SchemaCache{}) 256 require.NoError(t, err) 257 w, err := avro.ParseWithCache(test.writer, "", &avro.SchemaCache{}) 258 require.NoError(t, err) 259 sc := avro.NewSchemaCompatibility() 260 261 err = sc.Compatible(r, w) 262 263 test.wantErr(t, err) 264 }) 265 } 266 } 267 268 func TestSchemaCompatibility_CompatibleUsesCacheWithNoError(t *testing.T) { 269 reader := `"int"` 270 writer := `"int"` 271 272 r := avro.MustParse(reader) 273 w := avro.MustParse(writer) 274 sc := avro.NewSchemaCompatibility() 275 276 _ = sc.Compatible(r, w) 277 278 err := sc.Compatible(r, w) 279 280 assert.NoError(t, err) 281 } 282 283 func TestSchemaCompatibility_CompatibleUsesCacheWithError(t *testing.T) { 284 reader := `"int"` 285 writer := `"string"` 286 287 r := avro.MustParse(reader) 288 w := avro.MustParse(writer) 289 sc := avro.NewSchemaCompatibility() 290 291 _ = sc.Compatible(r, w) 292 293 err := sc.Compatible(r, w) 294 295 assert.Error(t, err) 296 } 297 298 func TestSchemaCompatibility_Resolve(t *testing.T) { 299 tests := []struct { 300 name string 301 reader string 302 writer string 303 value any 304 want any 305 }{ 306 { 307 name: "Int Promote Long", 308 reader: `"long"`, 309 writer: `"int"`, 310 value: 10, 311 want: int64(10), 312 }, 313 { 314 name: "Int Promote Long Time millis", 315 reader: `{"type":"long","logicalType":"timestamp-millis"}`, 316 writer: `"int"`, 317 value: 5000, 318 want: time.UnixMilli(5000).UTC(), 319 }, 320 { 321 name: "Int Promote Long Time micros", 322 reader: `{"type":"long","logicalType":"timestamp-micros"}`, 323 writer: `"int"`, 324 value: 5000, 325 want: time.UnixMicro(5000).UTC(), 326 }, 327 { 328 name: "Int Promote Long Time micros", 329 reader: `{"type":"long","logicalType":"time-micros"}`, 330 writer: `"int"`, 331 value: 5000, 332 want: 5000 * time.Microsecond, 333 }, 334 { 335 name: "Int Promote Float", 336 reader: `"float"`, 337 writer: `"int"`, 338 value: 10, 339 want: float32(10), 340 }, 341 { 342 name: "Int Promote Double", 343 reader: `"double"`, 344 writer: `"int"`, 345 value: 10, 346 want: float64(10), 347 }, 348 { 349 name: "Long Promote Float", 350 reader: `"float"`, 351 writer: `"long"`, 352 value: int64(10), 353 want: float32(10), 354 }, 355 { 356 name: "Long Promote Double", 357 reader: `"double"`, 358 writer: `"long"`, 359 value: int64(10), 360 want: float64(10), 361 }, 362 { 363 name: "Float Promote Double", 364 reader: `"double"`, 365 writer: `"float"`, 366 value: float32(10.5), 367 want: float64(10.5), 368 }, 369 { 370 name: "String Promote Bytes", 371 reader: `"bytes"`, 372 writer: `"string"`, 373 value: "foo", 374 want: []byte("foo"), 375 }, 376 { 377 // I'm not sure about this edge cases; 378 // I took the reverse path and tried to find a Decimal that can be encoded to 379 // a binary that is a valid UTF-8 sequence. 380 name: "String Promote Bytes With Logical Decimal", 381 reader: `{"type":"bytes","logicalType":"decimal","precision":4,"scale":2}`, 382 writer: `"string"`, 383 value: "d", 384 want: big.NewRat(1, 1), 385 }, 386 { 387 name: "Bytes Promote String", 388 reader: `"string"`, 389 writer: `"bytes"`, 390 value: []byte("foo"), 391 want: "foo", 392 }, 393 { 394 name: "Array With Items Promotion", 395 reader: `{"type":"array", "items": "long"}`, 396 writer: `{"type":"array", "items": "int"}`, 397 value: []any{int32(10), int32(15)}, 398 want: []any{int64(10), int64(15)}, 399 }, 400 { 401 name: "Map With Items Promotion", 402 reader: `{"type":"map", "values": "bytes"}`, 403 writer: `{"type":"map", "values": "string"}`, 404 value: map[string]any{"foo": "bar"}, 405 want: map[string]any{"foo": []byte("bar")}, 406 }, 407 { 408 name: "Enum Reader Missing Symbols With Default", 409 reader: `{ 410 "type": "enum", 411 "name": "test.enum", 412 "symbols": ["foo"], 413 "default": "foo" 414 }`, 415 writer: `{ 416 "type": "enum", 417 "name": "test.enum", 418 "symbols": ["foo", "bar"] 419 }`, 420 value: "bar", 421 want: "foo", 422 }, 423 { 424 name: "Enum Writer Missing Symbols", 425 reader: `{ 426 "type": "enum", 427 "name": "test.enum", 428 "symbols": ["foo", "bar"] 429 }`, 430 writer: `{ 431 "type": "enum", 432 "name": "test.enum", 433 "symbols": ["foo"] 434 }`, 435 value: "foo", 436 want: "foo", 437 }, 438 { 439 name: "Enum Writer Missing Symbols and Unused Reader Default", 440 reader: `{ 441 "type": "enum", 442 "name": "test.enum", 443 "symbols": ["foo", "bar"], 444 "default": "bar" 445 }`, 446 writer: `{ 447 "type": "enum", 448 "name": "test.enum", 449 "symbols": ["foo"] 450 }`, 451 value: "foo", 452 want: "foo", 453 }, 454 { 455 name: "Enum With Alias", 456 reader: `{ 457 "type": "enum", 458 "name": "test.enum2", 459 "aliases": ["test.enum"], 460 "symbols": ["foo", "bar"] 461 }`, 462 writer: `{ 463 "type": "enum", 464 "name": "test.enum", 465 "symbols": ["foo", "bar"] 466 }`, 467 value: "foo", 468 want: "foo", 469 }, 470 { 471 name: "Fixed With Alias", 472 reader: `{ 473 "type": "fixed", 474 "name": "test.fixed2", 475 "aliases": ["test.fixed"], 476 "size": 3 477 }`, 478 writer: `{ 479 "type": "fixed", 480 "name": "test.fixed", 481 "size": 3 482 }`, 483 value: [3]byte{'f', 'o', 'o'}, 484 want: [3]byte{'f', 'o', 'o'}, 485 }, 486 { 487 name: "Union Match", 488 reader: `["int", "long", "string"]`, 489 writer: `["string", "int", "long"]`, 490 value: "foo", 491 want: "foo", 492 }, { 493 name: "Union Writer Missing Schema", 494 reader: `["int", "long", "string"]`, 495 writer: `["string", "int"]`, 496 value: "foo", 497 want: "foo", 498 }, 499 { 500 name: "Union Writer Not Union", 501 reader: `["int", "long", "string"]`, 502 writer: `"int"`, 503 value: 10, 504 want: 10, 505 }, 506 { 507 name: "Union Reader Not Union", 508 reader: `"int"`, 509 writer: `["int"]`, 510 value: 10, 511 want: 10, 512 }, 513 { 514 name: "Record Reader With Alias", 515 reader: `{"type":"record", "name":"test2", "aliases": ["test"], "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 516 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 517 value: map[string]any{"a": 10}, 518 want: map[string]any{"a": 10}, 519 }, 520 { 521 name: "Record Reader Field Missing", 522 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 523 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "b", "type": "string"}, {"name": "a", "type": "int"}]}`, 524 value: map[string]any{"a": 10, "b": "foo"}, 525 want: map[string]any{"a": 10}, 526 }, 527 { 528 name: "Record Writer Field Missing With Default", 529 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}, {"name": "b", "type": "string", "default": "test"}]}`, 530 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 531 value: map[string]any{"a": 10}, 532 want: map[string]any{"a": 10, "b": "test"}, 533 }, 534 { 535 name: "Record Reader Field With Alias", 536 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "aa", "type": "int", "aliases": ["a"]}]}`, 537 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 538 value: map[string]any{"a": 10}, 539 want: map[string]any{"aa": 10}, 540 }, 541 { 542 name: "Record Reader Field With Alias And Promotion", 543 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "aa", "type": "double", "aliases": ["a"]}]}`, 544 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 545 value: map[string]any{"a": 10}, 546 want: map[string]any{"aa": float64(10)}, 547 }, 548 { 549 name: "Record Writer Field Missing With Bytes Default", 550 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}, {"name": "b", "type": "bytes", "default":"\u0066\u006f\u006f"}]}`, 551 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 552 value: map[string]any{"a": 10}, 553 want: map[string]any{"a": 10, "b": []byte("foo")}, 554 }, 555 { 556 name: "Record Writer Field Missing With Bytes Default", 557 reader: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}, {"name": "b", "type": "bytes", "default":"\u0066\u006f\u006f"}]}`, 558 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 559 value: map[string]any{"a": 10}, 560 want: map[string]any{"a": 10, "b": []byte("foo")}, 561 }, 562 { 563 name: "Record Writer Field Missing With Record Default", 564 reader: `{ 565 "type":"record", "name":"test", "namespace": "org.hamba.avro", 566 "fields":[ 567 {"name": "a", "type": "int"}, 568 { 569 "name": "b", 570 "type": { 571 "type": "record", 572 "name": "test.record", 573 "fields" : [ 574 {"name": "a", "type": "string"}, 575 {"name": "b", "type": "string"} 576 ] 577 }, 578 "default":{"a":"foo", "b": "bar"} 579 } 580 ] 581 }`, 582 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 583 value: map[string]any{"a": 10}, 584 want: map[string]any{"a": 10, "b": map[string]any{"a": "foo", "b": "bar"}}, 585 }, 586 { 587 // assert that we are not mistakenly using the wrong cached decoder. 588 // decoder cache must be aware of fields defaults. 589 name: "Record Writer Field Missing With Record Default 2", 590 reader: `{ 591 "type":"record", 592 "name":"test", 593 "namespace": "org.hamba.avro", 594 "fields":[ 595 {"name": "a", "type": "int"}, 596 { 597 "name": "b", 598 "type": { 599 "type": "record", 600 "name": "test.record", 601 "fields" : [ 602 {"name": "a", "type": "string"}, 603 {"name": "b", "type": "string"} 604 ] 605 }, 606 "default":{"a":"foo 2", "b": "bar 2"} 607 } 608 ] 609 }`, 610 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 611 value: map[string]any{"a": 10}, 612 want: map[string]any{"a": 10, "b": map[string]any{"a": "foo 2", "b": "bar 2"}}, 613 }, 614 { 615 name: "Record Writer Field Missing With Map Default", 616 reader: `{ 617 "type":"record", "name":"test", "namespace": "org.hamba.avro", 618 "fields":[ 619 {"name": "a", "type": "int"}, 620 { 621 "name": "b", 622 "type": { 623 "type": "map", "values": "string" 624 }, 625 "default":{"foo":"bar"} 626 } 627 ] 628 }`, 629 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 630 value: map[string]any{"a": 10}, 631 want: map[string]any{"a": 10, "b": map[string]any{"foo": "bar"}}, 632 }, 633 { 634 name: "Record Writer Field Missing With Array Default", 635 reader: `{ 636 "type":"record", "name":"test", "namespace": "org.hamba.avro", 637 "fields":[ 638 {"name": "a", "type": "int"}, 639 { 640 "name": "b", 641 "type": { 642 "type": "array", "items": "int" 643 }, 644 "default":[1, 2, 3, 4] 645 } 646 ] 647 }`, 648 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 649 value: map[string]any{"a": 10}, 650 want: map[string]any{"a": 10, "b": []any{1, 2, 3, 4}}, 651 }, 652 { 653 name: "Record Writer Field Missing With Union Null Default", 654 reader: `{ 655 "type":"record", "name":"test", "namespace": "org.hamba.avro", 656 "fields":[ 657 {"name": "a", "type": "int"}, 658 { 659 "name": "b", 660 "type":["null", "long"], 661 "default": null 662 } 663 ] 664 }`, 665 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 666 value: map[string]any{"a": 10}, 667 want: map[string]any{"a": 10, "b": nil}, 668 }, 669 { 670 name: "Record Writer Field Missing With Union Non-null Default", 671 reader: `{ 672 "type":"record", "name":"test", "namespace": "org.hamba.avro", 673 "fields":[ 674 {"name": "a", "type": "int"}, 675 { 676 "name": "b", 677 "type":["string", "long"], 678 "default": "bar" 679 } 680 ] 681 }`, 682 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 683 value: map[string]any{"a": 10}, 684 want: map[string]any{"a": 10, "b": "bar"}, 685 }, 686 { 687 name: "Record Writer Field Missing With Fixed Duration Default", 688 reader: `{ 689 "type":"record", "name":"test", "namespace": "org.hamba.avro", 690 "fields":[ 691 {"name": "a", "type": "int"}, 692 { 693 "name": "b", 694 "type": { 695 "type": "fixed", 696 "name": "test.fixed", 697 "logicalType":"duration", 698 "size":12 699 }, 700 "default": "\u000c\u0000\u0000\u0000\u0022\u0000\u0000\u0000\u0052\u00aa\u0008\u0000" 701 } 702 ] 703 }`, 704 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 705 value: map[string]any{"a": 10}, 706 want: map[string]any{ 707 "a": 10, 708 "b": avro.LogicalDuration{ 709 Months: uint32(12), 710 Days: uint32(34), 711 Milliseconds: uint32(567890), 712 }, 713 }, 714 }, 715 { 716 name: "Record Writer Field Missing With Fixed Logical Decimal Default", 717 reader: `{ 718 "type":"record", "name":"test", "namespace": "org.hamba.avro", 719 "fields":[ 720 {"name": "a", "type": "int"}, 721 { 722 "name": "b", 723 "type": { 724 "type": "fixed", 725 "name": "test.fixed", 726 "size": 6, 727 "logicalType":"decimal", 728 "precision":4, 729 "scale":2 730 }, 731 "default": "\u0000\u0000\u0000\u0000\u0087\u0078" 732 } 733 ] 734 }`, 735 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 736 value: map[string]any{"a": 10}, 737 want: map[string]any{ 738 "a": 10, 739 "b": big.NewRat(1734, 5), 740 }, 741 }, 742 { 743 name: "Record Writer Field Missing With Enum Duration Default", 744 reader: `{ 745 "type":"record", "name":"test", "namespace": "org.hamba.avro", 746 "fields":[ 747 {"name": "a", "type": "int"}, 748 { 749 "name": "b", 750 "type": { 751 "type": "enum", 752 "name": "test.enum", 753 "symbols": ["foo", "bar"] 754 }, 755 "default": "bar" 756 } 757 ] 758 }`, 759 writer: `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "a", "type": "int"}]}`, 760 value: map[string]any{"a": 10}, 761 want: map[string]any{ 762 "a": 10, 763 "b": "bar", 764 }, 765 }, 766 { 767 name: "Record Writer Field Missing With Ref Default", 768 reader: `{ 769 "type": "record", 770 "name": "parent", 771 "namespace": "org.hamba.avro", 772 "fields": [ 773 { 774 "name": "a", 775 "type": { 776 "type": "record", 777 "name": "embed", 778 "namespace": "org.hamba.avro", 779 "fields": [{ 780 "name": "a", 781 "type": "long" 782 }] 783 } 784 }, 785 { 786 "name": "b", 787 "type": "embed", 788 "default": {"a": 20} 789 } 790 ] 791 }`, 792 writer: `{ 793 "type": "record", 794 "name": "parent", 795 "namespace": "org.hamba.avro", 796 "fields": [ 797 { 798 "name": "a", 799 "type": { 800 "type": "record", 801 "name": "embed", 802 "namespace": "org.hamba.avro", 803 "fields": [{ 804 "name": "a", 805 "type": "long" 806 }] 807 } 808 } 809 ] 810 }`, 811 value: map[string]any{"a": map[string]any{"a": int64(10)}}, 812 want: map[string]any{ 813 "a": map[string]any{"a": int64(10)}, 814 "b": map[string]any{"a": int64(20)}, 815 }, 816 }, 817 } 818 819 for _, test := range tests { 820 test := test 821 t.Run(test.name, func(t *testing.T) { 822 t.Parallel() 823 824 r, err := avro.ParseWithCache(test.reader, "", &avro.SchemaCache{}) 825 require.NoError(t, err) 826 w, err := avro.ParseWithCache(test.writer, "", &avro.SchemaCache{}) 827 require.NoError(t, err) 828 sc := avro.NewSchemaCompatibility() 829 830 b, err := avro.Marshal(w, test.value) 831 assert.NoError(t, err) 832 833 sch, err := sc.Resolve(r, w) 834 assert.NoError(t, err) 835 836 var result any 837 err = avro.Unmarshal(sch, b, &result) 838 assert.NoError(t, err) 839 840 assert.Equal(t, test.want, result) 841 }) 842 } 843 } 844 845 func TestSchemaCompatibility_ResolveWithRefs(t *testing.T) { 846 sch1 := avro.MustParse(`{ 847 "type": "record", 848 "name": "test", 849 "fields" : [ 850 {"name": "a", "type": "string"} 851 ] 852 }`) 853 sch2 := avro.MustParse(`{ 854 "type": "record", 855 "name": "test", 856 "fields" : [ 857 {"name": "a", "type": "bytes"} 858 ] 859 }`) 860 861 r := avro.NewRefSchema(sch1.(*avro.RecordSchema)) 862 w := avro.NewRefSchema(sch2.(*avro.RecordSchema)) 863 864 sc := avro.NewSchemaCompatibility() 865 866 value := map[string]any{"a": []byte("foo")} 867 b, err := avro.Marshal(w, value) 868 assert.NoError(t, err) 869 870 sch, err := sc.Resolve(r, w) 871 assert.NoError(t, err) 872 873 var result any 874 err = avro.Unmarshal(sch, b, &result) 875 assert.NoError(t, err) 876 877 want := map[string]any{"a": "foo"} 878 assert.Equal(t, want, result) 879 } 880 881 func TestSchemaCompatibility_ResolveWithComplexUnion(t *testing.T) { 882 r := avro.MustParse(`[ 883 { 884 "type":"record", 885 "name":"testA", 886 "aliases": ["test1"], 887 "namespace": "org.hamba.avro", 888 "fields":[{"name": "a", "type": "long"}] 889 }, 890 { 891 "type":"record", 892 "name":"testB", 893 "aliases": ["test2"], 894 "namespace": "org.hamba.avro", 895 "fields":[{"name": "b", "type": "bytes"}] 896 } 897 ]`) 898 899 w := avro.MustParse(`{ 900 "type":"record", 901 "name":"test2", 902 "namespace": "org.hamba.avro", 903 "fields":[{"name": "b", "type": "string"}] 904 }`) 905 906 value := map[string]any{"b": "foo"} 907 b, err := avro.Marshal(w, value) 908 assert.NoError(t, err) 909 910 sc := avro.NewSchemaCompatibility() 911 sch, err := sc.Resolve(r, w) 912 assert.NoError(t, err) 913 914 var result any 915 err = avro.Unmarshal(sch, b, &result) 916 assert.NoError(t, err) 917 918 want := map[string]any{"b": []byte("foo")} 919 assert.Equal(t, want, result) 920 }