@@ -40,6 +40,7 @@ import (
40
40
"github.com/pingcap/tidb/pkg/sessionctx/variable"
41
41
"github.com/pingcap/tidb/pkg/types"
42
42
"github.com/pingcap/tidb/pkg/util/chunk"
43
+ "github.com/pingcap/tidb/pkg/util/intest"
43
44
"github.com/pingcap/tipb/go-tipb"
44
45
)
45
46
@@ -111,6 +112,18 @@ var (
111
112
_ builtinFunc = & builtinCastJSONAsJSONSig {}
112
113
)
113
114
115
+ const (
116
+ maxTinyBlobSize = 255
117
+ maxBlobSize = 65535
118
+ maxMediumBlobSize = 16777215
119
+ maxLongBlobSize = 4294967295
120
+ // These two are magic numbers to be compatible with MySQL.
121
+ // They are `MaxBlobSize * 4` and `MaxMediumBlobSize * 4`, but multiply by 4 (mblen) is not necessary here. However
122
+ // a bigger number is always safer to avoid truncation, so they are kept as is.
123
+ castBlobFlen = maxBlobSize * 4
124
+ castMediumBlobFlen = maxMediumBlobSize * 4
125
+ )
126
+
114
127
type castAsIntFunctionClass struct {
115
128
baseFunctionClass
116
129
@@ -299,6 +312,7 @@ func (c *castAsStringFunctionClass) getFunction(ctx sessionctx.Context, args []E
299
312
tp .AddFlag (mysql .BinaryFlag )
300
313
args [0 ] = BuildCastFunction (ctx , args [0 ], tp )
301
314
}
315
+ << << << < HEAD
302
316
argTp := args [0 ].GetType ().EvalType ()
303
317
switch argTp {
304
318
case types .ETInt :
@@ -319,6 +333,13 @@ func (c *castAsStringFunctionClass) getFunction(ctx sessionctx.Context, args []E
319
333
bf .tp .SetFlen (args [0 ].GetType ().GetFlen ())
320
334
}
321
335
}
336
+ == == == =
337
+ argFt := args [0 ].GetType (ctx .GetEvalCtx ())
338
+ adjustRetFtForCastString (bf .tp , argFt )
339
+
340
+ switch argFt .EvalType () {
341
+ case types .ETInt :
342
+ >> >> >> > 3 c2dc46853f (expression : fix the length of casting from INT / REAL / DECIMAL / ... . to string (#61476 ))
322
343
sig = & builtinCastIntAsStringSig {bf }
323
344
sig .setPbCode (tipb .ScalarFuncSig_CastIntAsString )
324
345
case types .ETReal :
@@ -343,11 +364,160 @@ func (c *castAsStringFunctionClass) getFunction(ctx sessionctx.Context, args []E
343
364
sig = & builtinCastStringAsStringSig {bf }
344
365
sig .setPbCode (tipb .ScalarFuncSig_CastStringAsString )
345
366
default :
367
+ << << << < HEAD
346
368
panic ("unsupported types.EvalType in castAsStringFunctionClass" )
369
+ == == == =
370
+ return nil , errors .Errorf ("cannot cast from %s to %s" , argFt .EvalType (), "String" )
371
+ >> >> >> > 3 c2dc46853f (expression : fix the length of casting from INT / REAL / DECIMAL / ... . to string (#61476 ))
347
372
}
348
373
return sig , nil
349
374
}
350
375
376
+ func adjustRetFtForCastString (retFt , argFt * types.FieldType ) {
377
+ originalFlen := retFt .GetFlen ()
378
+
379
+ // Only estimate the length for variable length string types, because different length for fixed
380
+ // length string types will have different behaviors and may cause compatibility issues.
381
+ if retFt .GetType () == mysql .TypeString {
382
+ return
383
+ }
384
+
385
+ if argFt .GetType () == mysql .TypeNull {
386
+ return
387
+ }
388
+
389
+ argTp := argFt .EvalType ()
390
+ switch argTp {
391
+ case types .ETInt :
392
+ if originalFlen == types .UnspecifiedLength {
393
+ // check https://github.com/pingcap/tidb/issues/44786
394
+ // set flen from integers may truncate integers, e.g. char(1) can not display -1[int(1)]
395
+ switch argFt .GetType () {
396
+ case mysql .TypeTiny :
397
+ if mysql .HasUnsignedFlag (argFt .GetFlag ()) {
398
+ retFt .SetFlen (3 )
399
+ } else {
400
+ retFt .SetFlen (4 )
401
+ }
402
+ case mysql .TypeShort :
403
+ if mysql .HasUnsignedFlag (argFt .GetFlag ()) {
404
+ retFt .SetFlen (5 )
405
+ } else {
406
+ retFt .SetFlen (6 )
407
+ }
408
+ case mysql .TypeInt24 :
409
+ if mysql .HasUnsignedFlag (argFt .GetFlag ()) {
410
+ retFt .SetFlen (8 )
411
+ } else {
412
+ retFt .SetFlen (9 )
413
+ }
414
+ case mysql .TypeLong :
415
+ if mysql .HasUnsignedFlag (argFt .GetFlag ()) {
416
+ retFt .SetFlen (10 )
417
+ } else {
418
+ retFt .SetFlen (11 )
419
+ }
420
+ case mysql .TypeLonglong :
421
+ // the length of BIGINT is always 20 without considering the unsigned flag, because the
422
+ // bigint range from -9223372036854775808 to 9223372036854775807, and unsigned bigint range
423
+ // from 0 to 18446744073709551615, they are all 20 characters long.
424
+ retFt .SetFlen (20 )
425
+ case mysql .TypeYear :
426
+ retFt .SetFlen (4 )
427
+ case mysql .TypeBit :
428
+ retFt .SetFlen (argFt .GetFlen ())
429
+ case mysql .TypeEnum :
430
+ intest .Assert (false , "cast Enum to String should not set mysql.EnumSetAsIntFlag" )
431
+ return
432
+ case mysql .TypeSet :
433
+ intest .Assert (false , "cast Set to String should not set mysql.EnumSetAsIntFlag" )
434
+ return
435
+ default :
436
+ intest .Assert (false , "unknown type %d for INT" , argFt .GetType ())
437
+ return
438
+ }
439
+ }
440
+ case types .ETReal :
441
+ // MySQL used 12/22 for float/double, it's because MySQL turns float/double into scientific notation
442
+ // in some situations. TiDB choose to use 'f' format for all the cases, so TiDB needs much longer length
443
+ // for float/double.
444
+ //
445
+ // The largest float/double value is around `3.40e38`/`1.79e308`, and the smallest positive float/double value
446
+ // is around `1.40e-45`/`4.94e-324`. Therefore, we need at least `1 (sign) + 1 (integer) + 1 (dot) + (45 + 39) (decimal) = 87`
447
+ // for float and `1 (sign) + 1 (integer) + 1 (dot) + (324 + 43) (decimal) = 370` for double.
448
+ //
449
+ // Actually, the golang will usually generate a much smaller string. It used ryu algorithm to generate the shortest
450
+ // decimal representation. It's not necessary to keep all decimals. Ref:
451
+ // - https://github.com/ulfjack/ryu
452
+ // - https://dl.acm.org/doi/10.1145/93548.93559
453
+ // So maybe 48/327 is enough for float/double, but we still set 87/370 for safety.
454
+ if originalFlen == types .UnspecifiedLength {
455
+ if argFt .GetType () == mysql .TypeFloat {
456
+ retFt .SetFlen (87 )
457
+ } else if argFt .GetType () == mysql .TypeDouble {
458
+ retFt .SetFlen (370 )
459
+ }
460
+ }
461
+ case types .ETDecimal :
462
+ if originalFlen == types .UnspecifiedLength {
463
+ retFt .SetFlen (decimalPrecisionToLength (argFt ))
464
+ }
465
+ case types .ETDatetime , types .ETTimestamp :
466
+ if originalFlen == types .UnspecifiedLength {
467
+ if argFt .GetType () == mysql .TypeDate {
468
+ retFt .SetFlen (mysql .MaxDateWidth )
469
+ } else {
470
+ retFt .SetFlen (mysql .MaxDatetimeWidthNoFsp )
471
+ }
472
+
473
+ // Theoretically, the decimal of `DATE` will never be greater than 0.
474
+ decimal := argFt .GetDecimal ()
475
+ if decimal > 0 {
476
+ // If the type is datetime or timestamp with fractional seconds, we need to set the length to
477
+ // accommodate the fractional seconds part.
478
+ retFt .SetFlen (retFt .GetFlen () + 1 + decimal )
479
+ }
480
+ }
481
+ case types .ETDuration :
482
+ if originalFlen == types .UnspecifiedLength {
483
+ retFt .SetFlen (mysql .MaxDurationWidthNoFsp )
484
+ decimal := argFt .GetDecimal ()
485
+ if decimal > 0 {
486
+ // If the type is time with fractional seconds, we need to set the length to
487
+ // accommodate the fractional seconds part.
488
+ retFt .SetFlen (retFt .GetFlen () + 1 + decimal )
489
+ }
490
+ }
491
+ case types .ETJson :
492
+ if originalFlen == types .UnspecifiedLength {
493
+ retFt .SetFlen (mysql .MaxLongBlobWidth )
494
+ retFt .SetType (mysql .TypeLongBlob )
495
+ }
496
+ case types .ETVectorFloat32 :
497
+
498
+ case types .ETString :
499
+ if originalFlen == types .UnspecifiedLength {
500
+ switch argFt .GetType () {
501
+ case mysql .TypeString , mysql .TypeVarchar , mysql .TypeVarString :
502
+ if argFt .GetFlen () > 0 {
503
+ retFt .SetFlen (argFt .GetFlen ())
504
+ }
505
+ case mysql .TypeTinyBlob :
506
+ retFt .SetFlen (maxTinyBlobSize )
507
+ case mysql .TypeBlob :
508
+ retFt .SetFlen (castBlobFlen )
509
+ case mysql .TypeMediumBlob :
510
+ retFt .SetFlen (castMediumBlobFlen )
511
+ case mysql .TypeLongBlob :
512
+ retFt .SetFlen (maxLongBlobSize )
513
+ default :
514
+ intest .Assert (false , "unknown type %d for String" , argFt .GetType ())
515
+ return
516
+ }
517
+ }
518
+ }
519
+ }
520
+
351
521
type castAsTimeFunctionClass struct {
352
522
baseFunctionClass
353
523
@@ -1056,7 +1226,12 @@ func (b *builtinCastRealAsStringSig) evalString(row chunk.Row) (res string, isNu
1056
1226
// If we strconv.FormatFloat the value with 64bits, the result is incorrect!
1057
1227
bits = 32
1058
1228
}
1229
+ << << << < HEAD
1059
1230
res , err = types .ProduceStrWithSpecifiedTp (strconv .FormatFloat (val , 'f' , - 1 , bits ), b .tp , b .ctx .GetSessionVars ().StmtCtx , false )
1231
+ == == == =
1232
+
1233
+ res , err = types .ProduceStrWithSpecifiedTp (strconv .FormatFloat (val , 'f' , - 1 , bits ), b .tp , typeCtx (ctx ), false )
1234
+ >> >> >> > 3 c2dc46853f (expression : fix the length of casting from INT / REAL / DECIMAL / ... . to string (#61476 ))
1060
1235
if err != nil {
1061
1236
return res , false , err
1062
1237
}
@@ -2442,3 +2617,27 @@ func TryPushCastIntoControlFunctionForHybridType(ctx sessionctx.Context, expr Ex
2442
2617
}
2443
2618
return expr
2444
2619
}
2620
+
2621
+ func decimalPrecisionToLength (ft * types.FieldType ) int {
2622
+ precision := ft .GetFlen ()
2623
+ scale := ft .GetDecimal ()
2624
+ unsigned := mysql .HasUnsignedFlag (ft .GetFlag ())
2625
+
2626
+ if precision == types .UnspecifiedLength || scale == types .UnspecifiedLength {
2627
+ return types .UnspecifiedLength
2628
+ }
2629
+
2630
+ ret := precision
2631
+ if scale > 0 {
2632
+ ret ++
2633
+ }
2634
+
2635
+ if ! unsigned && precision > 0 {
2636
+ ret ++ // for negative sign
2637
+ }
2638
+
2639
+ if ret == 0 {
2640
+ return 1
2641
+ }
2642
+ return ret
2643
+ }
0 commit comments