Skip to content

Commit 3046f14

Browse files
authored
fix: align explicit CAST overflow behavior with MySQL (#25898)
Explicit SQL `CAST` and `CONVERT` expressions previously reused the ordinary internal cast overload, whose strict overflow errors differ from MySQL's explicit-cast behavior. This change: - routes only MySQL `SIGNED`, `UNSIGNED`, and `DECIMAL` cast targets through a dedicated overload, keeping MatrixOne extension integer targets and planner-inserted casts unchanged; - applies MySQL-compatible signed/unsigned overflow handling across string, integer, float, Decimal64, Decimal128, and Decimal256 sources; - clamps explicit decimal casts across Decimal64, Decimal128, and Decimal256 target precisions; - preserves errors for invalid numeric strings; - adds planner/function unit tests, column-vector coverage, extension-target regression tests, and BVT coverage for the reported cases and boundaries. User impact: overflow cases such as large integer strings, negative-to-unsigned casts, and oversized decimals now produce MySQL-compatible values without changing existing strict semantics for implicit casts or MatrixOne extension integer targets. Validation: - `go test ./pkg/sql/plan/function ./pkg/sql/plan` - `go test -race ./pkg/sql/plan/function ./pkg/sql/plan` - `go vet ./pkg/sql/plan/function ./pkg/sql/plan` - `make build` - `func_cast.test` BVT: 226/226 passed - `boundary_comprehensive.sql` BVT: 241/241 passed - changed helper coverage: 75.0%-100% - `git diff --check` Approved by: @XuPeng-SH, @heni02
1 parent b3fe1ba commit 3046f14

17 files changed

Lines changed: 1648 additions & 63 deletions

pkg/sql/plan/base_binder.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,11 @@ func (b *baseBinder) baseBindExpr(astExpr tree.Expr, depth int32, isRoot bool) (
155155
if err != nil {
156156
return
157157
}
158-
expr, err = appendCastBeforeExpr(b.GetContext(), expr, typ)
158+
if useExplicitCastOverload(exprImpl.Type) {
159+
expr, err = appendExplicitCastBeforeExpr(b.GetContext(), expr, typ)
160+
} else {
161+
expr, err = appendCastBeforeExpr(b.GetContext(), expr, typ)
162+
}
159163

160164
case *tree.BitCastExpr:
161165
expr, err = b.bindFuncExprImplByAstExpr("bit_cast", []tree.Expr{astExpr}, depth)
@@ -281,6 +285,24 @@ func (b *baseBinder) baseBindExpr(astExpr tree.Expr, depth int32, isRoot bool) (
281285
return
282286
}
283287

288+
func useExplicitCastOverload(typ tree.ResolvableTypeReference) bool {
289+
t, ok := typ.(*tree.T)
290+
if !ok {
291+
return false
292+
}
293+
internal := t.InternalType
294+
switch defines.MysqlType(internal.Oid) {
295+
case defines.MYSQL_TYPE_DECIMAL, defines.MYSQL_TYPE_NEWDECIMAL:
296+
return true
297+
case defines.MYSQL_TYPE_LONGLONG:
298+
family := strings.ToLower(internal.FamilyString)
299+
return family == "signed" || family == "integer" ||
300+
(internal.Unsigned && (family == "" || family == "unsigned"))
301+
default:
302+
return false
303+
}
304+
}
305+
284306
func unwrapParenExpr(astExpr tree.Expr) tree.Expr {
285307
for {
286308
paren, ok := astExpr.(*tree.ParenExpr)
@@ -3000,12 +3022,22 @@ func (b *baseBinder) GetContext() context.Context { return b.sysCtx }
30003022
// --- util functions ----
30013023

30023024
func appendCastBeforeExpr(ctx context.Context, expr *Expr, toType Type, isBin ...bool) (*Expr, error) {
3025+
return appendCastBeforeExprWithOverload(ctx, expr, toType, 0, isBin...)
3026+
}
3027+
3028+
func appendExplicitCastBeforeExpr(ctx context.Context, expr *Expr, toType Type) (*Expr, error) {
3029+
return appendCastBeforeExprWithOverload(ctx, expr, toType, 1)
3030+
}
3031+
3032+
func appendCastBeforeExprWithOverload(
3033+
ctx context.Context, expr *Expr, toType Type, overloadID int32, isBin ...bool,
3034+
) (*Expr, error) {
30033035
toType.NotNullable = expr.Typ.NotNullable
30043036
argsType := []types.Type{
30053037
makeTypeByPlan2Expr(expr),
30063038
makeTypeByPlan2Type(toType),
30073039
}
3008-
fGet, err := function.GetFunctionByName(ctx, "cast", argsType)
3040+
fGet, err := function.GetFunctionByNameWithOverload(ctx, "cast", argsType, overloadID)
30093041
if err != nil {
30103042
return nil, err
30113043
}

pkg/sql/plan/explicit_cast_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2026 Matrix Origin
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package plan
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"github.com/matrixorigin/matrixone/pkg/container/types"
22+
"github.com/matrixorigin/matrixone/pkg/defines"
23+
"github.com/matrixorigin/matrixone/pkg/sql/parsers/tree"
24+
"github.com/matrixorigin/matrixone/pkg/sql/plan/function"
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
func TestExplicitCastUsesDedicatedOverload(t *testing.T) {
29+
ctx := context.Background()
30+
source := makePlan2StringConstExprWithType("1")
31+
targetType := types.T_int64.ToType()
32+
target := makePlan2TypeValue(&targetType)
33+
34+
ordinary, err := appendCastBeforeExpr(ctx, DeepCopyExpr(source), target)
35+
require.NoError(t, err)
36+
explicit, err := appendExplicitCastBeforeExpr(ctx, DeepCopyExpr(source), target)
37+
require.NoError(t, err)
38+
39+
ordinaryFunction := ordinary.GetF().GetFunc()
40+
explicitFunction := explicit.GetF().GetFunc()
41+
require.Equal(t, "cast", ordinaryFunction.GetObjName())
42+
require.Equal(t, "cast", explicitFunction.GetObjName())
43+
_, ordinaryOverload := function.DecodeOverloadID(ordinaryFunction.GetObj())
44+
_, explicitOverload := function.DecodeOverloadID(explicitFunction.GetObj())
45+
require.Equal(t, int32(0), ordinaryOverload)
46+
require.Equal(t, int32(1), explicitOverload)
47+
}
48+
49+
func TestUseExplicitCastOverload(t *testing.T) {
50+
tests := []struct {
51+
name string
52+
typ tree.InternalType
53+
want bool
54+
}{
55+
{name: "signed", typ: tree.InternalType{Oid: uint32(defines.MYSQL_TYPE_LONGLONG), FamilyString: "signed"}, want: true},
56+
{name: "signed integer", typ: tree.InternalType{Oid: uint32(defines.MYSQL_TYPE_LONGLONG), FamilyString: "integer"}, want: true},
57+
{name: "unsigned", typ: tree.InternalType{Oid: uint32(defines.MYSQL_TYPE_LONGLONG), Unsigned: true}, want: true},
58+
{name: "decimal", typ: tree.InternalType{Oid: uint32(defines.MYSQL_TYPE_NEWDECIMAL), FamilyString: "decimal"}, want: true},
59+
{name: "tinyint", typ: tree.InternalType{Oid: uint32(defines.MYSQL_TYPE_TINY), FamilyString: "tinyint"}},
60+
{name: "smallint", typ: tree.InternalType{Oid: uint32(defines.MYSQL_TYPE_SHORT), FamilyString: "smallint"}},
61+
{name: "int", typ: tree.InternalType{Oid: uint32(defines.MYSQL_TYPE_LONG), FamilyString: "int"}},
62+
{name: "bigint", typ: tree.InternalType{Oid: uint32(defines.MYSQL_TYPE_LONGLONG), FamilyString: "bigint"}},
63+
{name: "bigint unsigned", typ: tree.InternalType{Oid: uint32(defines.MYSQL_TYPE_LONGLONG), FamilyString: "bigint", Unsigned: true}},
64+
}
65+
for _, test := range tests {
66+
t.Run(test.name, func(t *testing.T) {
67+
require.Equal(t, test.want, useExplicitCastOverload(&tree.T{InternalType: test.typ}))
68+
})
69+
}
70+
}

0 commit comments

Comments
 (0)