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  }