github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/sink/dmlsink/txn/mysql/dml.go (about)

     1  // Copyright 2022 PingCAP, Inc.
     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  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package mysql
    15  
    16  import (
    17  	"strings"
    18  
    19  	"github.com/pingcap/tidb/pkg/parser/charset"
    20  	"github.com/pingcap/tiflow/cdc/model"
    21  	"github.com/pingcap/tiflow/pkg/quotes"
    22  )
    23  
    24  // prepareUpdate builds a parametrics UPDATE statement as following
    25  // sql: `UPDATE `test`.`t` SET {} = ?, {} = ? WHERE {} = ?, {} = {} LIMIT 1`
    26  // `WHERE` conditions come from `preCols` and SET clause targets come from `cols`.
    27  func prepareUpdate(quoteTable string, preCols, cols []*model.Column, forceReplicate bool) (string, []interface{}) {
    28  	var builder strings.Builder
    29  	builder.WriteString("UPDATE " + quoteTable + " SET ")
    30  
    31  	columnNames := make([]string, 0, len(cols))
    32  	args := make([]interface{}, 0, len(cols)+len(preCols))
    33  	for _, col := range cols {
    34  		if col == nil || col.Flag.IsGeneratedColumn() {
    35  			continue
    36  		}
    37  		columnNames = append(columnNames, col.Name)
    38  		args = appendQueryArgs(args, col)
    39  	}
    40  	if len(args) == 0 {
    41  		return "", nil
    42  	}
    43  	for i, column := range columnNames {
    44  		if i == len(columnNames)-1 {
    45  			builder.WriteString("`" + quotes.EscapeName(column) + "` = ?")
    46  		} else {
    47  			builder.WriteString("`" + quotes.EscapeName(column) + "` = ?, ")
    48  		}
    49  	}
    50  
    51  	builder.WriteString(" WHERE ")
    52  	colNames, wargs := whereSlice(preCols, forceReplicate)
    53  	if len(wargs) == 0 {
    54  		return "", nil
    55  	}
    56  	for i := 0; i < len(colNames); i++ {
    57  		if i > 0 {
    58  			builder.WriteString(" AND ")
    59  		}
    60  		if wargs[i] == nil {
    61  			builder.WriteString(quotes.QuoteName(colNames[i]) + " IS NULL")
    62  		} else {
    63  			builder.WriteString(quotes.QuoteName(colNames[i]) + " = ?")
    64  			args = append(args, wargs[i])
    65  		}
    66  	}
    67  	builder.WriteString(" LIMIT 1")
    68  	sql := builder.String()
    69  	return sql, args
    70  }
    71  
    72  // prepareReplace builds a parametrics REPLACE statement as following
    73  // sql: `REPLACE INTO `test`.`t` VALUES (?,?,?)`
    74  func prepareReplace(
    75  	quoteTable string,
    76  	cols []*model.Column,
    77  	appendPlaceHolder bool,
    78  	translateToInsert bool,
    79  ) (string, []interface{}) {
    80  	var builder strings.Builder
    81  	columnNames := make([]string, 0, len(cols))
    82  	args := make([]interface{}, 0, len(cols))
    83  	for _, col := range cols {
    84  		if col == nil || col.Flag.IsGeneratedColumn() {
    85  			continue
    86  		}
    87  		columnNames = append(columnNames, col.Name)
    88  		args = appendQueryArgs(args, col)
    89  	}
    90  	if len(args) == 0 {
    91  		return "", nil
    92  	}
    93  
    94  	colList := "(" + buildColumnList(columnNames) + ")"
    95  	if translateToInsert {
    96  		builder.WriteString("INSERT INTO " + quoteTable + " " + colList + " VALUES ")
    97  	} else {
    98  		builder.WriteString("REPLACE INTO " + quoteTable + " " + colList + " VALUES ")
    99  	}
   100  	if appendPlaceHolder {
   101  		builder.WriteString("(" + placeHolder(len(columnNames)) + ")")
   102  	}
   103  
   104  	return builder.String(), args
   105  }
   106  
   107  // if the column value type is []byte and charset is not binary, we get its string
   108  // representation. Because if we use the byte array respresentation, the go-sql-driver
   109  // will automatically set `_binary` charset for that column, which is not expected.
   110  // See https://github.com/go-sql-driver/mysql/blob/ce134bfc/connection.go#L267
   111  func appendQueryArgs(args []interface{}, col *model.Column) []interface{} {
   112  	if col.Charset != "" && col.Charset != charset.CharsetBin {
   113  		colValBytes, ok := col.Value.([]byte)
   114  		if ok {
   115  			args = append(args, string(colValBytes))
   116  		} else {
   117  			args = append(args, col.Value)
   118  		}
   119  	} else {
   120  		args = append(args, col.Value)
   121  	}
   122  
   123  	return args
   124  }
   125  
   126  // prepareDelete builds a parametric DELETE statement as following
   127  // sql: `DELETE FROM `test`.`t` WHERE x = ? AND y >= ? LIMIT 1`
   128  func prepareDelete(quoteTable string, cols []*model.Column, forceReplicate bool) (string, []interface{}) {
   129  	var builder strings.Builder
   130  	builder.WriteString("DELETE FROM " + quoteTable + " WHERE ")
   131  
   132  	colNames, wargs := whereSlice(cols, forceReplicate)
   133  	if len(wargs) == 0 {
   134  		return "", nil
   135  	}
   136  	args := make([]interface{}, 0, len(wargs))
   137  	for i := 0; i < len(colNames); i++ {
   138  		if i > 0 {
   139  			builder.WriteString(" AND ")
   140  		}
   141  		if wargs[i] == nil {
   142  			builder.WriteString(quotes.QuoteName(colNames[i]) + " IS NULL")
   143  		} else {
   144  			builder.WriteString(quotes.QuoteName(colNames[i]) + " = ?")
   145  			args = append(args, wargs[i])
   146  		}
   147  	}
   148  	builder.WriteString(" LIMIT 1")
   149  	sql := builder.String()
   150  	return sql, args
   151  }
   152  
   153  // whereSlice builds a parametric WHERE clause as following
   154  // sql: `WHERE {} = ? AND {} > ?`
   155  func whereSlice(cols []*model.Column, forceReplicate bool) (colNames []string, args []interface{}) {
   156  	// Try to use unique key values when available
   157  	for _, col := range cols {
   158  		if col == nil || !col.Flag.IsHandleKey() {
   159  			continue
   160  		}
   161  		colNames = append(colNames, col.Name)
   162  		args = appendQueryArgs(args, col)
   163  	}
   164  	// if no explicit row id but force replicate, use all key-values in where condition
   165  	if len(colNames) == 0 && forceReplicate {
   166  		colNames = make([]string, 0, len(cols))
   167  		args = make([]interface{}, 0, len(cols))
   168  		for _, col := range cols {
   169  			colNames = append(colNames, col.Name)
   170  			args = appendQueryArgs(args, col)
   171  		}
   172  	}
   173  	return
   174  }
   175  
   176  func buildColumnList(names []string) string {
   177  	var b strings.Builder
   178  	for i, name := range names {
   179  		if i > 0 {
   180  			b.WriteString(",")
   181  		}
   182  		b.WriteString(quotes.QuoteName(name))
   183  
   184  	}
   185  
   186  	return b.String()
   187  }
   188  
   189  // placeHolder returns a string separated by comma
   190  // n must be greater or equal than 1, or the function will panic
   191  func placeHolder(n int) string {
   192  	var builder strings.Builder
   193  	builder.Grow((n-1)*2 + 1)
   194  	for i := 0; i < n; i++ {
   195  		if i > 0 {
   196  			builder.WriteString(",")
   197  		}
   198  		builder.WriteString("?")
   199  	}
   200  	return builder.String()
   201  }