Skip to content

Commit 16b9431

Browse files
authored
Merge branch 'main' into update_bvt
2 parents 5ad13e5 + 54d9635 commit 16b9431

File tree

2 files changed

+180
-1
lines changed

2 files changed

+180
-1
lines changed

pkg/sql/plan/function/func_cast.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4396,6 +4396,10 @@ func strToStr(
43964396
}
43974397
return nil
43984398
}
4399+
// Get source type to check if it's TEXT
4400+
fromType := from.GetSourceVector().GetType()
4401+
isSourceText := fromType.Oid == types.T_text
4402+
43994403
if totype.Oid != types.T_text && destLen != 0 {
44004404
for i = 0; i < l; i++ {
44014405
v, null := from.GetStrValue(i)
@@ -4407,7 +4411,24 @@ func strToStr(
44074411
}
44084412
// check the length.
44094413
s := convertByteSliceToString(v)
4410-
if utf8.RuneCountInString(s) > destLen {
4414+
// For explicit CAST operations (e.g., CAST(text_col AS CHAR(1))), we should
4415+
// always perform length validation, even if source is TEXT, because the user
4416+
// explicitly requested a specific type with a length limit.
4417+
//
4418+
// However, for implicit conversions in UPDATE statements where the target
4419+
// column is actually TEXT but misidentified as CHAR/VARCHAR, we should skip
4420+
// length validation. We distinguish this by checking the target width:
4421+
// - Small widths (like 1, 10, etc.) are likely explicit CASTs and should be validated
4422+
// - Large widths (>= 255) might be misidentified TEXT columns in UPDATE operations
4423+
//
4424+
// The threshold of 255 is chosen because:
4425+
// 1. It's a common default width for TEXT columns that get misidentified
4426+
// 2. Explicit CASTs to CHAR(255) are rare, and when they occur, the user
4427+
// likely expects validation (though we skip it for compatibility)
4428+
// 3. This allows UPDATE operations on TEXT columns to work correctly
4429+
shouldSkipLengthCheck := isSourceText && (toType.Oid == types.T_char || toType.Oid == types.T_varchar) && destLen >= 255
4430+
4431+
if !shouldSkipLengthCheck && utf8.RuneCountInString(s) > destLen {
44114432
return formatCastError(ctx, from.GetSourceVector(), totype, fmt.Sprintf(
44124433
"Src length %v is larger than Dest length %v", len(s), destLen))
44134434
}

pkg/sql/plan/function/func_cast_test.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"fmt"
2020
"math"
21+
"strings"
2122
"testing"
2223
"time"
2324

@@ -1953,3 +1954,160 @@ func Benchmark_strToSigned_Binary(b *testing.B) {
19531954
})
19541955
}
19551956
}
1957+
1958+
// Test_strToStr_TextToCharVarchar tests that TEXT type can be cast to CHAR/VARCHAR
1959+
// without length validation errors, even when the string length exceeds the target length.
1960+
// This is important for UPDATE operations on TEXT columns with CONCAT operations.
1961+
func Test_strToStr_TextToCharVarchar(t *testing.T) {
1962+
ctx := context.Background()
1963+
mp := mpool.MustNewZero()
1964+
1965+
// Helper function to create long strings
1966+
longString260 := strings.Repeat("a", 260) // 260 characters
1967+
longString100 := strings.Repeat("b", 100) // 100 characters
1968+
1969+
tests := []struct {
1970+
name string
1971+
inputs []string
1972+
nulls []uint64
1973+
fromType types.Type
1974+
toType types.Type
1975+
want []string
1976+
wantNulls []uint64
1977+
wantErr bool
1978+
errMsg string
1979+
}{
1980+
{
1981+
name: "TEXT to CHAR(255) with length 260 - should succeed",
1982+
inputs: []string{longString260},
1983+
fromType: types.T_text.ToType(),
1984+
toType: types.New(types.T_char, 255, 0),
1985+
want: []string{longString260}, // Should keep original length
1986+
wantErr: false,
1987+
},
1988+
{
1989+
name: "TEXT to VARCHAR(255) with length 260 - should succeed",
1990+
inputs: []string{longString260},
1991+
fromType: types.T_text.ToType(),
1992+
toType: types.New(types.T_varchar, 255, 0),
1993+
want: []string{longString260}, // Should keep original length
1994+
wantErr: false,
1995+
},
1996+
{
1997+
name: "TEXT to CHAR(255) with NULL - should handle NULL",
1998+
inputs: []string{"", "test"},
1999+
nulls: []uint64{0},
2000+
fromType: types.T_text.ToType(),
2001+
toType: types.New(types.T_char, 255, 0),
2002+
want: []string{"", "test"},
2003+
wantNulls: []uint64{0},
2004+
wantErr: false,
2005+
},
2006+
{
2007+
name: "VARCHAR to CHAR(10) with length 100 - should fail (normal behavior)",
2008+
inputs: []string{longString100},
2009+
fromType: types.New(types.T_varchar, 100, 0),
2010+
toType: types.New(types.T_char, 10, 0),
2011+
wantErr: true,
2012+
errMsg: "larger than Dest length",
2013+
},
2014+
{
2015+
name: "TEXT to CHAR(1) with length > 1 - should fail (explicit CAST)",
2016+
inputs: []string{"ab"},
2017+
fromType: types.T_text.ToType(),
2018+
toType: types.New(types.T_char, 1, 0),
2019+
wantErr: true,
2020+
errMsg: "larger than Dest length",
2021+
},
2022+
{
2023+
name: "TEXT to CHAR(10) with length 100 - should fail (explicit CAST to small width)",
2024+
inputs: []string{longString100},
2025+
fromType: types.T_text.ToType(),
2026+
toType: types.New(types.T_char, 10, 0),
2027+
wantErr: true,
2028+
errMsg: "larger than Dest length",
2029+
},
2030+
{
2031+
name: "TEXT to VARCHAR(10) with length 100 - should fail (explicit CAST to small width)",
2032+
inputs: []string{longString100},
2033+
fromType: types.T_text.ToType(),
2034+
toType: types.New(types.T_varchar, 10, 0),
2035+
wantErr: true,
2036+
errMsg: "larger than Dest length",
2037+
},
2038+
{
2039+
name: "TEXT to TEXT - should succeed",
2040+
inputs: []string{"test text"},
2041+
fromType: types.T_text.ToType(),
2042+
toType: types.T_text.ToType(),
2043+
want: []string{"test text"},
2044+
wantErr: false,
2045+
},
2046+
{
2047+
name: "TEXT to CHAR(255) with multiple values",
2048+
inputs: []string{"short", longString260, "medium length string"},
2049+
fromType: types.T_text.ToType(),
2050+
toType: types.New(types.T_char, 255, 0),
2051+
want: []string{"short", longString260, "medium length string"},
2052+
wantErr: false,
2053+
},
2054+
}
2055+
2056+
for _, tt := range tests {
2057+
t.Run(tt.name, func(t *testing.T) {
2058+
// Create input vector based on source type
2059+
var inputVec *vector.Vector
2060+
if tt.fromType.Oid == types.T_text {
2061+
inputVec = testutil.MakeTextVector(tt.inputs, tt.nulls)
2062+
} else {
2063+
inputVec = testutil.MakeVarcharVector(tt.inputs, tt.nulls)
2064+
// Set the type explicitly for non-TEXT types
2065+
inputVec.SetType(tt.fromType)
2066+
}
2067+
defer inputVec.Free(mp)
2068+
2069+
from := vector.GenerateFunctionStrParameter(inputVec)
2070+
2071+
resultType := tt.toType
2072+
to := vector.NewFunctionResultWrapper(resultType, mp).(*vector.FunctionResult[types.Varlena])
2073+
defer to.Free()
2074+
err := to.PreExtendAndReset(len(tt.inputs))
2075+
require.NoError(t, err)
2076+
2077+
err = strToStr(ctx, from, to, len(tt.inputs), tt.toType)
2078+
2079+
if tt.wantErr {
2080+
require.Error(t, err)
2081+
if tt.errMsg != "" {
2082+
require.Contains(t, err.Error(), tt.errMsg)
2083+
}
2084+
return
2085+
}
2086+
require.NoError(t, err)
2087+
2088+
resultVec := to.GetResultVector()
2089+
r := vector.GenerateFunctionStrParameter(resultVec)
2090+
2091+
for i := 0; i < len(tt.want); i++ {
2092+
want := tt.want[i]
2093+
get, null := r.GetStrValue(uint64(i))
2094+
2095+
if contains(tt.wantNulls, uint64(i)) {
2096+
require.True(t, null, "row %d should be null", i)
2097+
} else {
2098+
require.False(t, null, "row %d should not be null", i)
2099+
require.Equal(t, want, string(get), "row %d value not match", i)
2100+
}
2101+
}
2102+
2103+
resultNulls := to.GetResultVector().GetNulls()
2104+
if len(tt.wantNulls) > 0 {
2105+
for _, pos := range tt.wantNulls {
2106+
require.True(t, resultNulls.Contains(pos))
2107+
}
2108+
} else {
2109+
require.True(t, resultNulls.IsEmpty())
2110+
}
2111+
})
2112+
}
2113+
}

0 commit comments

Comments
 (0)