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  }