github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ccl/changefeedccl/avro_test.go (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Licensed as a CockroachDB Enterprise file under the Cockroach Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt 8 9 package changefeedccl 10 11 import ( 12 "context" 13 "encoding/json" 14 "fmt" 15 "math" 16 "math/rand" 17 "strings" 18 "testing" 19 "time" 20 21 "github.com/cockroachdb/apd" 22 "github.com/cockroachdb/cockroach/pkg/ccl/importccl" 23 "github.com/cockroachdb/cockroach/pkg/keys" 24 "github.com/cockroachdb/cockroach/pkg/settings/cluster" 25 "github.com/cockroachdb/cockroach/pkg/sql/parser" 26 "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" 27 "github.com/cockroachdb/cockroach/pkg/sql/sessiondata" 28 "github.com/cockroachdb/cockroach/pkg/sql/sqlbase" 29 "github.com/cockroachdb/cockroach/pkg/sql/types" 30 "github.com/cockroachdb/cockroach/pkg/util/hlc" 31 "github.com/cockroachdb/cockroach/pkg/util/leaktest" 32 "github.com/cockroachdb/cockroach/pkg/util/randutil" 33 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 34 "github.com/cockroachdb/errors" 35 "github.com/stretchr/testify/require" 36 ) 37 38 func parseTableDesc(createTableStmt string) (*sqlbase.TableDescriptor, error) { 39 ctx := context.Background() 40 stmt, err := parser.ParseOne(createTableStmt) 41 if err != nil { 42 return nil, errors.Wrapf(err, `parsing %s`, createTableStmt) 43 } 44 createTable, ok := stmt.AST.(*tree.CreateTable) 45 if !ok { 46 return nil, errors.Errorf("expected *tree.CreateTable got %T", stmt) 47 } 48 st := cluster.MakeTestingClusterSettings() 49 const parentID = sqlbase.ID(keys.MaxReservedDescID + 1) 50 const tableID = sqlbase.ID(keys.MaxReservedDescID + 2) 51 mutDesc, err := importccl.MakeSimpleTableDescriptor( 52 ctx, st, createTable, parentID, tableID, importccl.NoFKs, hlc.UnixNano()) 53 if err != nil { 54 return nil, err 55 } 56 return mutDesc.TableDesc(), mutDesc.TableDesc().ValidateTable() 57 } 58 59 func parseValues(tableDesc *sqlbase.TableDescriptor, values string) ([]sqlbase.EncDatumRow, error) { 60 ctx := context.Background() 61 semaCtx := tree.MakeSemaContext() 62 evalCtx := &tree.EvalContext{} 63 64 valuesStmt, err := parser.ParseOne(values) 65 if err != nil { 66 return nil, err 67 } 68 selectStmt, ok := valuesStmt.AST.(*tree.Select) 69 if !ok { 70 return nil, errors.Errorf("expected *tree.Select got %T", valuesStmt) 71 } 72 valuesClause, ok := selectStmt.Select.(*tree.ValuesClause) 73 if !ok { 74 return nil, errors.Errorf("expected *tree.ValuesClause got %T", selectStmt.Select) 75 } 76 77 var rows []sqlbase.EncDatumRow 78 for _, rowTuple := range valuesClause.Rows { 79 var row sqlbase.EncDatumRow 80 for colIdx, expr := range rowTuple { 81 col := &tableDesc.Columns[colIdx] 82 typedExpr, err := sqlbase.SanitizeVarFreeExpr( 83 ctx, expr, col.Type, "avro", &semaCtx, false /* allowImpure */) 84 if err != nil { 85 return nil, err 86 } 87 datum, err := typedExpr.Eval(evalCtx) 88 if err != nil { 89 return nil, errors.Wrapf(err, "evaluating %s", typedExpr) 90 } 91 row = append(row, sqlbase.DatumToEncDatum(col.Type, datum)) 92 } 93 rows = append(rows, row) 94 } 95 return rows, nil 96 } 97 98 func parseAvroSchema(j string) (*avroDataRecord, error) { 99 var s avroDataRecord 100 if err := json.Unmarshal([]byte(j), &s); err != nil { 101 return nil, err 102 } 103 // This avroDataRecord doesn't have any of the derived fields we need for 104 // serde. Instead of duplicating the logic, fake out a TableDescriptor, so 105 // we can reuse tableToAvroSchema and get them for free. 106 tableDesc := &sqlbase.TableDescriptor{ 107 Name: AvroNameToSQLName(s.Name), 108 } 109 for _, f := range s.Fields { 110 // s.Fields[idx] has `Name` and `SchemaType` set but nothing else. 111 // They're needed for serialization/deserialization, so fake out a 112 // column descriptor so that we can reuse columnDescToAvroSchema to get 113 // all the various fields of avroSchemaField populated for free. 114 colDesc, err := avroFieldMetadataToColDesc(f.Metadata) 115 if err != nil { 116 return nil, err 117 } 118 tableDesc.Columns = append(tableDesc.Columns, *colDesc) 119 } 120 return tableToAvroSchema(tableDesc, avroSchemaNoSuffix) 121 } 122 123 func avroFieldMetadataToColDesc(metadata string) (*sqlbase.ColumnDescriptor, error) { 124 parsed, err := parser.ParseOne(`ALTER TABLE FOO ADD COLUMN ` + metadata) 125 if err != nil { 126 return nil, err 127 } 128 def := parsed.AST.(*tree.AlterTable).Cmds[0].(*tree.AlterTableAddColumn).ColumnDef 129 ctx := context.Background() 130 semaCtx := tree.MakeSemaContext() 131 col, _, _, err := sqlbase.MakeColumnDefDescs(ctx, def, &semaCtx, &tree.EvalContext{}) 132 return col, err 133 } 134 135 // randTime generates a random time.Time whose .UnixNano result doesn't 136 // overflow an int64. 137 func randTime(rng *rand.Rand) time.Time { 138 return timeutil.Unix(0, rng.Int63()) 139 } 140 141 func TestAvroSchema(t *testing.T) { 142 defer leaktest.AfterTest(t)() 143 rng, _ := randutil.NewPseudoRand() 144 145 type test struct { 146 name string 147 schema string 148 values string 149 } 150 tests := []test{ 151 { 152 name: `NULLABLE`, 153 schema: `(a INT PRIMARY KEY, b INT NULL)`, 154 values: `(1, 2), (3, NULL)`, 155 }, 156 { 157 name: `TUPLE`, 158 schema: `(a INT PRIMARY KEY, b STRING)`, 159 values: `(1, 'a')`, 160 }, 161 { 162 name: `MULTI_WIDTHS`, 163 schema: `(a INT PRIMARY KEY, b DECIMAL (3,2), c DECIMAL (2, 1))`, 164 values: `(1, 1.23, 4.5)`, 165 }, 166 } 167 // Generate a test for each column type with a random datum of that type. 168 for _, typ := range types.OidToType { 169 switch typ.Family() { 170 case types.AnyFamily, types.OidFamily, types.TupleFamily: 171 // These aren't expected to be needed for changefeeds. 172 continue 173 case types.IntervalFamily, types.ArrayFamily, types.BitFamily, 174 types.CollatedStringFamily: 175 // Implement these as customer demand dictates. 176 continue 177 } 178 datum := sqlbase.RandDatum(rng, typ, false /* nullOk */) 179 if datum == tree.DNull { 180 // DNull is returned by RandDatum for types.UNKNOWN or if the 181 // column type is unimplemented in RandDatum. In either case, the 182 // correct thing to do is skip this one. 183 continue 184 } 185 switch typ.Family() { 186 case types.TimestampFamily: 187 // Truncate to millisecond instead of microsecond because of a bug 188 // in the avro lib's deserialization code. The serialization seems 189 // to be fine and we only use deserialization for testing, so we 190 // should patch the bug but it's not currently affecting changefeed 191 // correctness. 192 // TODO(mjibson): goavro mishandles timestamps 193 // whose nanosecond representation overflows an 194 // int64, so restrict input to fit. 195 t := randTime(rng).Truncate(time.Millisecond) 196 datum = tree.MustMakeDTimestamp(t, time.Microsecond) 197 case types.TimestampTZFamily: 198 // See comments above for TimestampFamily. 199 t := randTime(rng).Truncate(time.Millisecond) 200 datum = tree.MustMakeDTimestampTZ(t, time.Microsecond) 201 case types.DecimalFamily: 202 // TODO(dan): Make RandDatum respect Precision and Width instead. 203 // TODO(dan): The precision is really meant to be in [1,10], but it 204 // sure looks like there's an off by one error in the avro library 205 // that makes this test flake if it picks precision of 1. 206 precision := rng.Int31n(10) + 2 207 scale := rng.Int31n(precision + 1) 208 typ = types.MakeDecimal(precision, scale) 209 coeff := rng.Int63n(int64(math.Pow10(int(precision)))) 210 datum = &tree.DDecimal{Decimal: *apd.New(coeff, -scale)} 211 case types.DateFamily: 212 // TODO(mjibson): goavro mishandles dates whose 213 // nanosecond representation overflows an int64, 214 // so restrict input to fit. 215 var err error 216 datum, err = tree.NewDDateFromTime(randTime(rng)) 217 if err != nil { 218 panic(err) 219 } 220 } 221 serializedDatum := tree.Serialize(datum) 222 // name can be "char" (with quotes), so needs to be escaped. 223 escapedName := fmt.Sprintf("%s_table", strings.Replace(typ.String(), "\"", "", -1)) 224 // schema is used in a fmt.Sprintf to fill in the table name, so we have 225 // to escape any stray %s. 226 escapedDatum := strings.Replace(serializedDatum, `%`, `%%`, -1) 227 randTypeTest := test{ 228 name: escapedName, 229 schema: fmt.Sprintf(`(a INT PRIMARY KEY, b %s)`, typ.SQLString()), 230 values: fmt.Sprintf(`(1, %s)`, escapedDatum), 231 } 232 tests = append(tests, randTypeTest) 233 } 234 235 for _, test := range tests { 236 t.Run(test.name, func(t *testing.T) { 237 tableDesc, err := parseTableDesc( 238 fmt.Sprintf(`CREATE TABLE "%s" %s`, test.name, test.schema)) 239 require.NoError(t, err) 240 origSchema, err := tableToAvroSchema(tableDesc, avroSchemaNoSuffix) 241 require.NoError(t, err) 242 jsonSchema := origSchema.codec.Schema() 243 roundtrippedSchema, err := parseAvroSchema(jsonSchema) 244 require.NoError(t, err) 245 // It would require some work, but we could also check that the 246 // roundtrippedSchema can be used to recreate the original `CREATE 247 // TABLE`. 248 249 rows, err := parseValues(tableDesc, `VALUES `+test.values) 250 require.NoError(t, err) 251 252 for _, row := range rows { 253 evalCtx := &tree.EvalContext{SessionData: &sessiondata.SessionData{}} 254 serialized, err := origSchema.textualFromRow(row) 255 require.NoError(t, err) 256 roundtripped, err := roundtrippedSchema.rowFromTextual(serialized) 257 require.NoError(t, err) 258 require.Equal(t, 0, row[1].Datum.Compare(evalCtx, roundtripped[1].Datum), 259 `%s != %s`, row[1].Datum, roundtripped[1].Datum) 260 261 serialized, err = origSchema.BinaryFromRow(nil, row) 262 require.NoError(t, err) 263 roundtripped, err = roundtrippedSchema.RowFromBinary(serialized) 264 require.NoError(t, err) 265 require.Equal(t, 0, row[1].Datum.Compare(evalCtx, roundtripped[1].Datum), 266 `%s != %s`, row[1].Datum, roundtripped[1].Datum) 267 } 268 }) 269 } 270 271 t.Run("escaping", func(t *testing.T) { 272 tableDesc, err := parseTableDesc(`CREATE TABLE "☃" (🍦 INT PRIMARY KEY)`) 273 require.NoError(t, err) 274 tableSchema, err := tableToAvroSchema(tableDesc, avroSchemaNoSuffix) 275 require.NoError(t, err) 276 require.Equal(t, 277 `{"type":"record","name":"_u2603_","fields":[`+ 278 `{"type":["null","long"],"name":"_u0001f366_","default":null,`+ 279 `"__crdb__":"🍦 INT8 NOT NULL"}]}`, 280 tableSchema.codec.Schema()) 281 indexSchema, err := indexToAvroSchema(tableDesc, &tableDesc.PrimaryIndex) 282 require.NoError(t, err) 283 require.Equal(t, 284 `{"type":"record","name":"_u2603_","fields":[`+ 285 `{"type":["null","long"],"name":"_u0001f366_","default":null,`+ 286 `"__crdb__":"🍦 INT8 NOT NULL"}]}`, 287 indexSchema.codec.Schema()) 288 }) 289 290 // This test shows what avro schema each sql column maps to, for easy 291 // reference. 292 t.Run("type_goldens", func(t *testing.T) { 293 goldens := map[string]string{ 294 `BOOL`: `["null","boolean"]`, 295 `BYTES`: `["null","bytes"]`, 296 `DATE`: `["null",{"type":"int","logicalType":"date"}]`, 297 `FLOAT8`: `["null","double"]`, 298 `GEOGRAPHY`: `["null","bytes"]`, 299 `GEOMETRY`: `["null","bytes"]`, 300 `INET`: `["null","string"]`, 301 `INT8`: `["null","long"]`, 302 `JSONB`: `["null","string"]`, 303 `STRING`: `["null","string"]`, 304 `TIME`: `["null",{"type":"long","logicalType":"time-micros"}]`, 305 `TIMETZ`: `["null","string"]`, 306 `TIMESTAMP`: `["null",{"type":"long","logicalType":"timestamp-micros"}]`, 307 `TIMESTAMPTZ`: `["null",{"type":"long","logicalType":"timestamp-micros"}]`, 308 `UUID`: `["null","string"]`, 309 `DECIMAL(3,2)`: `["null",{"type":"bytes","logicalType":"decimal","precision":3,"scale":2}]`, 310 } 311 312 for _, typ := range types.Scalar { 313 switch typ.Family() { 314 case types.IntervalFamily, types.OidFamily, types.BitFamily: 315 continue 316 case types.DecimalFamily: 317 typ = types.MakeDecimal(3, 2) 318 } 319 320 colType := typ.SQLString() 321 tableDesc, err := parseTableDesc(`CREATE TABLE foo (pk INT PRIMARY KEY, a ` + colType + `)`) 322 require.NoError(t, err) 323 field, err := columnDescToAvroSchema(&tableDesc.Columns[1]) 324 require.NoError(t, err) 325 schema, err := json.Marshal(field.SchemaType) 326 require.NoError(t, err) 327 require.Equal(t, goldens[colType], string(schema), `SQL type %s`, colType) 328 329 // Delete from goldens for the following assertion that we don't have any 330 // unexpectedly unused goldens. 331 delete(goldens, colType) 332 } 333 if len(goldens) > 0 { 334 t.Fatalf("expected all goldens to be consumed: %v", goldens) 335 } 336 }) 337 338 // This test shows what avro value some sql datums map to, for easy reference. 339 // The avro golden strings are in the textual format defined in the spec. 340 t.Run("value_goldens", func(t *testing.T) { 341 goldens := []struct { 342 sqlType string 343 sql string 344 avro string 345 }{ 346 {sqlType: `INT`, sql: `NULL`, avro: `null`}, 347 {sqlType: `INT`, 348 sql: `1`, 349 avro: `{"long":1}`}, 350 351 {sqlType: `BOOL`, sql: `NULL`, avro: `null`}, 352 {sqlType: `BOOL`, 353 sql: `true`, 354 avro: `{"boolean":true}`}, 355 356 {sqlType: `FLOAT`, sql: `NULL`, avro: `null`}, 357 {sqlType: `FLOAT`, 358 sql: `1.2`, 359 avro: `{"double":1.2}`}, 360 361 {sqlType: `GEOGRAPHY`, sql: `NULL`, avro: `null`}, 362 {sqlType: `GEOGRAPHY`, 363 sql: "'POINT(1.0 1.0)'", 364 avro: `{"bytes":"\u0001\u0001\u0000\u0000 \u00E6\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00F0?\u0000\u0000\u0000\u0000\u0000\u0000\u00F0?"}`}, 365 {sqlType: `GEOMETRY`, sql: `NULL`, avro: `null`}, 366 {sqlType: `GEOMETRY`, 367 sql: "'POINT(1.0 1.0)'", 368 avro: `{"bytes":"\u0001\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00F0?\u0000\u0000\u0000\u0000\u0000\u0000\u00F0?"}`}, 369 370 {sqlType: `STRING`, sql: `NULL`, avro: `null`}, 371 {sqlType: `STRING`, 372 sql: `'foo'`, 373 avro: `{"string":"foo"}`}, 374 375 {sqlType: `BYTES`, sql: `NULL`, avro: `null`}, 376 {sqlType: `BYTES`, 377 sql: `'foo'`, 378 avro: `{"bytes":"foo"}`}, 379 380 {sqlType: `DATE`, sql: `NULL`, avro: `null`}, 381 {sqlType: `DATE`, 382 sql: `'2019-01-02'`, 383 avro: `{"int.date":17898}`}, 384 385 {sqlType: `TIME`, sql: `NULL`, avro: `null`}, 386 {sqlType: `TIME`, 387 sql: `'03:04:05'`, 388 avro: `{"long.time-micros":11045000000}`}, 389 390 {sqlType: `TIMESTAMP`, sql: `NULL`, avro: `null`}, 391 {sqlType: `TIMESTAMP`, 392 sql: `'2019-01-02 03:04:05'`, 393 avro: `{"long.timestamp-micros":1546398245000000}`}, 394 395 {sqlType: `TIMESTAMPTZ`, sql: `NULL`, avro: `null`}, 396 {sqlType: `TIMESTAMPTZ`, 397 sql: `'2019-01-02 03:04:05'`, 398 avro: `{"long.timestamp-micros":1546398245000000}`}, 399 400 {sqlType: `DECIMAL(4,1)`, sql: `NULL`, avro: `null`}, 401 {sqlType: `DECIMAL(4,1)`, 402 sql: `1.2`, 403 avro: `{"bytes.decimal":"\f"}`}, 404 405 {sqlType: `UUID`, sql: `NULL`, avro: `null`}, 406 {sqlType: `UUID`, 407 sql: `'27f4f4c9-e35a-45dd-9b79-5ff0f9b5fbb0'`, 408 avro: `{"string":"27f4f4c9-e35a-45dd-9b79-5ff0f9b5fbb0"}`}, 409 410 {sqlType: `INET`, sql: `NULL`, avro: `null`}, 411 {sqlType: `INET`, 412 sql: `'190.0.0.0'`, 413 avro: `{"string":"190.0.0.0"}`}, 414 {sqlType: `INET`, 415 sql: `'190.0.0.0/24'`, 416 avro: `{"string":"190.0.0.0\/24"}`}, 417 {sqlType: `INET`, 418 sql: `'2001:4f8:3:ba:2e0:81ff:fe22:d1f1'`, 419 avro: `{"string":"2001:4f8:3:ba:2e0:81ff:fe22:d1f1"}`}, 420 {sqlType: `INET`, 421 sql: `'2001:4f8:3:ba:2e0:81ff:fe22:d1f1/120'`, 422 avro: `{"string":"2001:4f8:3:ba:2e0:81ff:fe22:d1f1\/120"}`}, 423 {sqlType: `INET`, 424 sql: `'::ffff:192.168.0.1/24'`, 425 avro: `{"string":"::ffff:192.168.0.1\/24"}`}, 426 427 {sqlType: `JSONB`, sql: `NULL`, avro: `null`}, 428 {sqlType: `JSONB`, 429 sql: `'null'`, 430 avro: `{"string":"null"}`}, 431 {sqlType: `JSONB`, 432 sql: `'{"b": 1}'`, 433 avro: `{"string":"{\"b\": 1}"}`}, 434 } 435 436 for _, test := range goldens { 437 tableDesc, err := parseTableDesc( 438 `CREATE TABLE foo (pk INT PRIMARY KEY, a ` + test.sqlType + `)`) 439 require.NoError(t, err) 440 rows, err := parseValues(tableDesc, `VALUES (1, `+test.sql+`)`) 441 require.NoError(t, err) 442 443 schema, err := tableToAvroSchema(tableDesc, avroSchemaNoSuffix) 444 require.NoError(t, err) 445 textual, err := schema.textualFromRow(rows[0]) 446 require.NoError(t, err) 447 // Trim the outermost {}. 448 value := string(textual[1 : len(textual)-1]) 449 // Strip out the pk field. 450 value = strings.Replace(value, `"pk":{"long":1}`, ``, -1) 451 // Trim the `,`, which could be on either side because of the avro library 452 // doesn't deterministically order the fields. 453 value = strings.Trim(value, `,`) 454 // Strip out the field name. 455 value = strings.Replace(value, `"a":`, ``, -1) 456 require.Equal(t, test.avro, value) 457 } 458 }) 459 } 460 461 func (f *avroSchemaField) defaultValueNative() (interface{}, bool) { 462 schemaType := f.SchemaType 463 if union, ok := schemaType.([]avroSchemaType); ok { 464 // "Default values for union fields correspond to the first schema in 465 // the union." 466 schemaType = union[0] 467 } 468 switch schemaType { 469 case avroSchemaNull: 470 return nil, true 471 } 472 panic(errors.Errorf(`unimplemented %T: %v`, schemaType, schemaType)) 473 } 474 475 // rowFromBinaryEvolved decodes `buf` using writerSchema but evolves/resolves it 476 // to readerSchema using the rules from the avro spec: 477 // https://avro.apache.org/docs/1.8.2/spec.html#Schema+Resolution 478 // 479 // It'd be nice if our avro library handled this for us, but neither of the 480 // popular golang once seem to have it implemented. 481 func rowFromBinaryEvolved( 482 buf []byte, writerSchema, readerSchema *avroDataRecord, 483 ) (sqlbase.EncDatumRow, error) { 484 native, newBuf, err := writerSchema.codec.NativeFromBinary(buf) 485 if err != nil { 486 return nil, err 487 } 488 if len(newBuf) > 0 { 489 return nil, errors.New(`only one row was expected`) 490 } 491 nativeMap, ok := native.(map[string]interface{}) 492 if !ok { 493 return nil, errors.Errorf(`unknown avro native type: %T`, native) 494 } 495 adjustNative(nativeMap, writerSchema, readerSchema) 496 return readerSchema.rowFromNative(nativeMap) 497 } 498 499 func adjustNative(native map[string]interface{}, writerSchema, readerSchema *avroDataRecord) { 500 for _, writerField := range writerSchema.Fields { 501 if _, inReader := readerSchema.fieldIdxByName[writerField.Name]; !inReader { 502 // "If the writer's record contains a field with a name not present 503 // in the reader's record, the writer's value for that field is 504 // ignored." 505 delete(native, writerField.Name) 506 } 507 } 508 for _, readerField := range readerSchema.Fields { 509 if _, inWriter := writerSchema.fieldIdxByName[readerField.Name]; !inWriter { 510 // "If the reader's record schema has a field that contains a 511 // default value, and writer's schema does not have a field with the 512 // same name, then the reader should use the default value from its 513 // field." 514 if readerFieldDefault, ok := readerField.defaultValueNative(); ok { 515 native[readerField.Name] = readerFieldDefault 516 } 517 } 518 } 519 } 520 521 func TestAvroMigration(t *testing.T) { 522 defer leaktest.AfterTest(t)() 523 524 type test struct { 525 name string 526 writerSchema string 527 writerValues string 528 readerSchema string 529 expectedValues string 530 } 531 tests := []test{ 532 { 533 name: `add_nullable`, 534 writerSchema: `(a INT PRIMARY KEY)`, 535 writerValues: `(1)`, 536 readerSchema: `(a INT PRIMARY KEY, b INT)`, 537 expectedValues: `(1, NULL)`, 538 }, 539 } 540 for _, test := range tests { 541 t.Run(test.name, func(t *testing.T) { 542 writerDesc, err := parseTableDesc( 543 fmt.Sprintf(`CREATE TABLE "%s" %s`, test.name, test.writerSchema)) 544 require.NoError(t, err) 545 writerSchema, err := tableToAvroSchema(writerDesc, avroSchemaNoSuffix) 546 require.NoError(t, err) 547 readerDesc, err := parseTableDesc( 548 fmt.Sprintf(`CREATE TABLE "%s" %s`, test.name, test.readerSchema)) 549 require.NoError(t, err) 550 readerSchema, err := tableToAvroSchema(readerDesc, avroSchemaNoSuffix) 551 require.NoError(t, err) 552 553 writerRows, err := parseValues(writerDesc, `VALUES `+test.writerValues) 554 require.NoError(t, err) 555 expectedRows, err := parseValues(readerDesc, `VALUES `+test.expectedValues) 556 require.NoError(t, err) 557 558 for i := range writerRows { 559 writerRow, expectedRow := writerRows[i], expectedRows[i] 560 encoded, err := writerSchema.BinaryFromRow(nil, writerRow) 561 require.NoError(t, err) 562 row, err := rowFromBinaryEvolved(encoded, writerSchema, readerSchema) 563 require.NoError(t, err) 564 require.Equal(t, expectedRow, row) 565 } 566 }) 567 } 568 } 569 570 func TestDecimalRatRoundtrip(t *testing.T) { 571 defer leaktest.AfterTest(t)() 572 573 t.Run(`table`, func(t *testing.T) { 574 tests := []struct { 575 scale int32 576 dec *apd.Decimal 577 }{ 578 {0, apd.New(0, 0)}, 579 {0, apd.New(1, 0)}, 580 {0, apd.New(-1, 0)}, 581 {0, apd.New(123, 0)}, 582 {1, apd.New(0, -1)}, 583 {1, apd.New(1, -1)}, 584 {1, apd.New(123, -1)}, 585 {5, apd.New(1, -5)}, 586 } 587 for d, test := range tests { 588 rat, err := decimalToRat(*test.dec, test.scale) 589 require.NoError(t, err) 590 roundtrip := ratToDecimal(rat, test.scale) 591 if test.dec.CmpTotal(&roundtrip) != 0 { 592 t.Errorf(`%d: %s != %s`, d, test.dec, &roundtrip) 593 } 594 } 595 }) 596 t.Run(`error`, func(t *testing.T) { 597 _, err := decimalToRat(*apd.New(1, -2), 1) 598 require.EqualError(t, err, "0.01 will not roundtrip at scale 1") 599 _, err = decimalToRat(*apd.New(1, -1), 2) 600 require.EqualError(t, err, "0.1 will not roundtrip at scale 2") 601 _, err = decimalToRat(apd.Decimal{Form: apd.Infinite}, 0) 602 require.EqualError(t, err, "cannot convert Infinite form decimal") 603 }) 604 t.Run(`rand`, func(t *testing.T) { 605 rng, _ := randutil.NewPseudoRand() 606 precision := rng.Int31n(10) + 1 607 scale := rng.Int31n(precision + 1) 608 coeff := rng.Int63n(int64(math.Pow10(int(precision)))) 609 dec := apd.New(coeff, -scale) 610 rat, err := decimalToRat(*dec, scale) 611 require.NoError(t, err) 612 roundtrip := ratToDecimal(rat, scale) 613 if dec.CmpTotal(&roundtrip) != 0 { 614 t.Errorf(`%s != %s`, dec, &roundtrip) 615 } 616 }) 617 }