vitess.io/vitess@v0.16.2/go/mysql/collations/remote/collation.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 remote
    18  
    19  import (
    20  	"encoding/hex"
    21  	"fmt"
    22  	"io"
    23  	"math"
    24  	"strings"
    25  	"sync"
    26  
    27  	"vitess.io/vitess/go/bytes2"
    28  	"vitess.io/vitess/go/mysql"
    29  	"vitess.io/vitess/go/mysql/collations"
    30  	"vitess.io/vitess/go/mysql/collations/internal/charset"
    31  	"vitess.io/vitess/go/sqltypes"
    32  )
    33  
    34  // Collation is a generic implementation of the Collation interface
    35  // that supports any collation in MySQL by performing the collation
    36  // operation directly on a remote `mysqld` instance. It is not particularly
    37  // efficient compared to the native Collation implementations in Vitess,
    38  // but it offers authoritative results for all collation types and can be
    39  // used as a fallback or as a way to test our native implementations.
    40  type Collation struct {
    41  	name string
    42  	id   collations.ID
    43  
    44  	charset string
    45  	prefix  string
    46  	suffix  string
    47  
    48  	mu   sync.Mutex
    49  	conn *mysql.Conn
    50  	sql  bytes2.Buffer
    51  	hex  io.Writer
    52  	err  error
    53  }
    54  
    55  var _ collations.Collation = (*Collation)(nil)
    56  
    57  func makeRemoteCollation(conn *mysql.Conn, collid collations.ID, collname string) *Collation {
    58  	charset := collname
    59  	if idx := strings.IndexByte(collname, '_'); idx >= 0 {
    60  		charset = collname[:idx]
    61  	}
    62  
    63  	coll := &Collation{
    64  		name:    collname,
    65  		id:      collid,
    66  		conn:    conn,
    67  		charset: charset,
    68  	}
    69  
    70  	coll.prefix = fmt.Sprintf("_%s X'", charset)
    71  	coll.suffix = fmt.Sprintf("' COLLATE %q", collname)
    72  	coll.hex = hex.NewEncoder(&coll.sql)
    73  	return coll
    74  }
    75  
    76  func NewCollation(conn *mysql.Conn, collname string) *Collation {
    77  	return makeRemoteCollation(conn, collations.Unknown, collname)
    78  }
    79  
    80  func (c *Collation) LastError() error {
    81  	c.mu.Lock()
    82  	defer c.mu.Unlock()
    83  	return c.err
    84  }
    85  
    86  func (c *Collation) Init() {}
    87  
    88  func (c *Collation) ID() collations.ID {
    89  	return c.id
    90  }
    91  
    92  func (c *Collation) IsBinary() bool {
    93  	return false
    94  }
    95  
    96  func (c *Collation) Name() string {
    97  	return c.name
    98  }
    99  
   100  func (c *Collation) Charset() charset.Charset {
   101  	return makeRemoteCharset(c.conn, &c.mu, c.charset)
   102  }
   103  
   104  func (c *Collation) Collate(left, right []byte, isPrefix bool) int {
   105  	if isPrefix {
   106  		panic("unsupported: isPrefix with remote.Collation")
   107  	}
   108  
   109  	c.mu.Lock()
   110  	defer c.mu.Unlock()
   111  
   112  	c.sql.Reset()
   113  	c.sql.WriteString("SELECT STRCMP(")
   114  	c.sql.WriteString(c.prefix)
   115  	c.hex.Write(left)
   116  	c.sql.WriteString(c.suffix)
   117  	c.sql.WriteString(", ")
   118  	c.sql.WriteString(c.prefix)
   119  	c.hex.Write(right)
   120  	c.sql.WriteString(c.suffix)
   121  	c.sql.WriteString(")")
   122  
   123  	var cmp int64
   124  	if result := c.performRemoteQuery(); result != nil {
   125  		cmp, c.err = result[0].ToInt64()
   126  	}
   127  	return int(cmp)
   128  }
   129  
   130  func (c *Collation) performRemoteQuery() []sqltypes.Value {
   131  	res, err := c.conn.ExecuteFetch(c.sql.StringUnsafe(), 1, false)
   132  	if err != nil {
   133  		c.err = err
   134  		return nil
   135  	}
   136  	if len(res.Rows) != 1 {
   137  		c.err = fmt.Errorf("unexpected result from MySQL: %d rows returned", len(res.Rows))
   138  		return nil
   139  	}
   140  	c.err = nil
   141  	return res.Rows[0]
   142  }
   143  
   144  func (c *Collation) WeightString(dst, src []byte, numCodepoints int) []byte {
   145  	if numCodepoints == math.MaxInt32 {
   146  		panic("unsupported: PadToMax with remote.Collation")
   147  	}
   148  
   149  	c.mu.Lock()
   150  	defer c.mu.Unlock()
   151  
   152  	c.sql.Reset()
   153  	c.sql.WriteString("SELECT WEIGHT_STRING(")
   154  	c.sql.WriteString(c.prefix)
   155  	c.hex.Write(src)
   156  	c.sql.WriteString(c.suffix)
   157  	if numCodepoints > 0 {
   158  		fmt.Fprintf(&c.sql, " AS CHAR(%d)", numCodepoints)
   159  	}
   160  	c.sql.WriteString(")")
   161  
   162  	if result := c.performRemoteQuery(); result != nil {
   163  		resultBytes, _ := result[0].ToBytes()
   164  		if dst == nil {
   165  			dst = resultBytes
   166  		} else {
   167  			dst = append(dst, resultBytes...)
   168  		}
   169  	}
   170  	return dst
   171  }
   172  
   173  func (c *Collation) Hash(_ []byte, _ int) collations.HashCode {
   174  	panic("unsupported: Hash for remote collations")
   175  }
   176  
   177  type remotePattern struct {
   178  	remote  *Collation
   179  	pattern string
   180  	escape  rune
   181  }
   182  
   183  func (rp *remotePattern) Match(in []byte) bool {
   184  	c := rp.remote
   185  
   186  	c.mu.Lock()
   187  	defer c.mu.Unlock()
   188  
   189  	c.sql.Reset()
   190  	c.sql.WriteString("SELECT ")
   191  	c.sql.WriteString(c.prefix)
   192  	c.hex.Write(in)
   193  	c.sql.WriteString(c.suffix)
   194  	c.sql.WriteString(" LIKE ")
   195  	c.sql.WriteString(rp.pattern)
   196  
   197  	if rp.escape != 0 && rp.escape != '\\' {
   198  		fmt.Fprintf(&c.sql, " ESCAPE X'%x'", string(rp.escape))
   199  	}
   200  
   201  	var match bool
   202  	if result := c.performRemoteQuery(); result != nil {
   203  		match, c.err = result[0].ToBool()
   204  	}
   205  	return match
   206  }
   207  
   208  func (c *Collation) Wildcard(pat []byte, _ rune, _ rune, escape rune) collations.WildcardPattern {
   209  	return &remotePattern{
   210  		pattern: fmt.Sprintf("_%s X'%x'", c.charset, pat),
   211  		remote:  c,
   212  		escape:  escape,
   213  	}
   214  }
   215  
   216  func (c *Collation) WeightStringLen(_ int) int {
   217  	return 0
   218  }