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 }