vitess.io/vitess@v0.16.2/go/mysql/collations/integration/coercion_test.go (about) 1 /* 2 Copyright 2021 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package integration 18 19 import ( 20 "bytes" 21 "fmt" 22 "sort" 23 "strings" 24 "testing" 25 26 "github.com/stretchr/testify/assert" 27 "github.com/stretchr/testify/require" 28 29 "vitess.io/vitess/go/mysql/collations" 30 "vitess.io/vitess/go/mysql/collations/remote" 31 "vitess.io/vitess/go/sqltypes" 32 ) 33 34 type TextWithCollation struct { 35 Text []byte 36 Collation collations.Collation 37 } 38 39 type RemoteCoercionResult struct { 40 Expr sqltypes.Value 41 Collation collations.Collation 42 Coercibility collations.Coercibility 43 } 44 45 type RemoteCoercionTest interface { 46 Expression() string 47 Test(t *testing.T, remote *RemoteCoercionResult, local collations.TypedCollation, coerce1, coerce2 collations.Coercion) 48 } 49 50 type testConcat struct { 51 left, right *TextWithCollation 52 } 53 54 func (tc *testConcat) Expression() string { 55 return fmt.Sprintf("CONCAT((_%s X'%x' COLLATE %q), (_%s X'%x' COLLATE %q))", 56 tc.left.Collation.Charset().Name(), tc.left.Text, tc.left.Collation.Name(), 57 tc.right.Collation.Charset().Name(), tc.right.Text, tc.right.Collation.Name(), 58 ) 59 } 60 61 func (tc *testConcat) Test(t *testing.T, remote *RemoteCoercionResult, local collations.TypedCollation, coercion1, coercion2 collations.Coercion) { 62 localCollation := collations.Local().LookupByID(local.Collation) 63 assert.Equal(t, remote.Collation.Name(), localCollation.Name(), "bad collation resolved: local is %s, remote is %s", localCollation.Name(), remote.Collation.Name()) 64 assert.Equal(t, remote.Coercibility, local.Coercibility, "bad coercibility resolved: local is %d, remote is %d", local.Coercibility, remote.Coercibility) 65 66 leftText, err := coercion1(nil, tc.left.Text) 67 if err != nil { 68 t.Errorf("failed to transcode left: %v", err) 69 return 70 } 71 72 rightText, err := coercion2(nil, tc.right.Text) 73 if err != nil { 74 t.Errorf("failed to transcode right: %v", err) 75 return 76 } 77 78 var concat bytes.Buffer 79 concat.Write(leftText) 80 concat.Write(rightText) 81 82 rEBytes, err := remote.Expr.ToBytes() 83 require.NoError(t, err) 84 assert.True(t, bytes.Equal(concat.Bytes(), rEBytes), "failed to concatenate text;\n\tCONCAT(%v COLLATE %s, %v COLLATE %s) = \n\tCONCAT(%v, %v) COLLATE %s = \n\t\t%v\n\n\texpected: %v", tc.left.Text, tc.left.Collation.Name(), 85 tc.right.Text, tc.right.Collation.Name(), leftText, rightText, localCollation.Name(), 86 concat.Bytes(), rEBytes) 87 88 } 89 90 type testComparison struct { 91 left, right *TextWithCollation 92 } 93 94 func (tc *testComparison) Expression() string { 95 return fmt.Sprintf("(_%s X'%x' COLLATE %q) = (_%s X'%x' COLLATE %q)", 96 tc.left.Collation.Charset().Name(), tc.left.Text, tc.left.Collation.Name(), 97 tc.right.Collation.Charset().Name(), tc.right.Text, tc.right.Collation.Name(), 98 ) 99 } 100 101 func (tc *testComparison) Test(t *testing.T, remote *RemoteCoercionResult, local collations.TypedCollation, coerce1, coerce2 collations.Coercion) { 102 localCollation := collations.Local().LookupByID(local.Collation) 103 leftText, err := coerce1(nil, tc.left.Text) 104 if err != nil { 105 t.Errorf("failed to transcode left: %v", err) 106 return 107 } 108 109 rightText, err := coerce2(nil, tc.right.Text) 110 if err != nil { 111 t.Errorf("failed to transcode right: %v", err) 112 return 113 } 114 rEBytes, err := remote.Expr.ToBytes() 115 require.NoError(t, err) 116 remoteEquals := rEBytes[0] == '1' 117 localEquals := localCollation.Collate(leftText, rightText, false) == 0 118 assert.Equal(t, localEquals, remoteEquals, "failed to collate %#v = %#v with collation %s (expected %v, got %v)", leftText, rightText, localCollation.Name(), remoteEquals, localEquals) 119 120 } 121 122 func TestComparisonSemantics(t *testing.T) { 123 const BaseString = "abcdABCD01234" 124 var testInputs []*TextWithCollation 125 126 conn := mysqlconn(t) 127 defer conn.Close() 128 129 if v, err := conn.ServerVersionAtLeast(8, 0, 31); err != nil || !v { 130 t.Skipf("The behavior of Coercion Semantics is not correct before 8.0.31") 131 } 132 133 for _, coll := range collations.Local().AllCollations() { 134 text := verifyTranscoding(t, coll, remote.NewCollation(conn, coll.Name()), []byte(BaseString)) 135 testInputs = append(testInputs, &TextWithCollation{Text: text, Collation: coll}) 136 } 137 sort.Slice(testInputs, func(i, j int) bool { 138 return testInputs[i].Collation.ID() < testInputs[j].Collation.ID() 139 }) 140 141 var testCases = []struct { 142 name string 143 make func(left, right *TextWithCollation) RemoteCoercionTest 144 }{ 145 { 146 name: "equals", 147 make: func(left, right *TextWithCollation) RemoteCoercionTest { 148 return &testComparison{left, right} 149 }, 150 }, 151 { 152 name: "concat", 153 make: func(left, right *TextWithCollation) RemoteCoercionTest { 154 return &testConcat{left, right} 155 }, 156 }, 157 } 158 159 for _, tc := range testCases { 160 t.Run(tc.name, func(t *testing.T) { 161 for _, collA := range testInputs { 162 for _, collB := range testInputs { 163 left := collations.TypedCollation{ 164 Collation: collA.Collation.ID(), 165 Coercibility: 0, 166 Repertoire: collations.RepertoireASCII, 167 } 168 right := collations.TypedCollation{ 169 Collation: collB.Collation.ID(), 170 Coercibility: 0, 171 Repertoire: collations.RepertoireASCII, 172 } 173 resultLocal, coercionLocal1, coercionLocal2, errLocal := collations.Local().MergeCollations(left, right, 174 collations.CoercionOptions{ 175 ConvertToSuperset: true, 176 ConvertWithCoercion: true, 177 }) 178 179 // for strings that do not coerce, replace with a no-op coercion function 180 if coercionLocal1 == nil { 181 coercionLocal1 = func(_, in []byte) ([]byte, error) { return in, nil } 182 } 183 if coercionLocal2 == nil { 184 coercionLocal2 = func(_, in []byte) ([]byte, error) { return in, nil } 185 } 186 187 remoteTest := tc.make(collA, collB) 188 expr := remoteTest.Expression() 189 query := fmt.Sprintf("SELECT CAST((%s) AS BINARY), COLLATION(%s), COERCIBILITY(%s)", expr, expr, expr) 190 191 resultRemote, errRemote := conn.ExecuteFetch(query, 1, false) 192 if errRemote != nil { 193 require.True(t, strings.Contains(errRemote.Error(), "Illegal mix of collations"), "query %s failed: %v", query, errRemote) 194 195 if errLocal == nil { 196 t.Errorf("expected %s vs %s to fail coercion: %v", collA.Collation.Name(), collB.Collation.Name(), errRemote) 197 continue 198 } 199 require.True(t, strings.HasPrefix(normalizeCollationInError(errRemote.Error()), normalizeCollationInError(errLocal.Error())), "bad error message: expected %q, got %q", errRemote, errLocal) 200 201 continue 202 } 203 204 if errLocal != nil { 205 t.Errorf("expected %s vs %s to coerce, but they failed: %v", collA.Collation.Name(), collB.Collation.Name(), errLocal) 206 continue 207 } 208 209 remoteCollation := collations.Local().LookupByName(resultRemote.Rows[0][1].ToString()) 210 remoteCI, _ := resultRemote.Rows[0][2].ToInt64() 211 remoteTest.Test(t, &RemoteCoercionResult{ 212 Expr: resultRemote.Rows[0][0], 213 Collation: remoteCollation, 214 Coercibility: collations.Coercibility(remoteCI), 215 }, resultLocal, coercionLocal1, coercionLocal2) 216 } 217 } 218 }) 219 } 220 } 221 222 // normalizeCollationInError normalizes the collation name in the error output. 223 // Starting with mysql 8.0.30 collations prefixed with `utf8_` have been changed to use `utf8mb3_` instead 224 // This is inconsistent with older MySQL versions and causes the tests to fail against it. 225 // As a stop-gap solution, this functions normalizes the error messages so that the tests pass until we 226 // have a fix for it. 227 // TODO: Remove error normalization 228 func normalizeCollationInError(errMessage string) string { 229 return strings.ReplaceAll(errMessage, "utf8_", "utf8mb3_") 230 }