github.com/vedadiyan/sqlparser@v1.0.0/pkg/sqlparser/comments.go (about) 1 /* 2 Copyright 2019 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 sqlparser 18 19 import ( 20 "strconv" 21 "strings" 22 "unicode" 23 24 querypb "github.com/vedadiyan/sqlparser/pkg/query" 25 ) 26 27 const ( 28 // DirectiveMultiShardAutocommit is the query comment directive to allow 29 // single round trip autocommit with a multi-shard statement. 30 DirectiveMultiShardAutocommit = "MULTI_SHARD_AUTOCOMMIT" 31 // DirectiveSkipQueryPlanCache skips query plan cache when set. 32 DirectiveSkipQueryPlanCache = "SKIP_QUERY_PLAN_CACHE" 33 // DirectiveQueryTimeout sets a query timeout in vtgate. Only supported for SELECTS. 34 DirectiveQueryTimeout = "QUERY_TIMEOUT_MS" 35 // DirectiveScatterErrorsAsWarnings enables partial success scatter select queries 36 DirectiveScatterErrorsAsWarnings = "SCATTER_ERRORS_AS_WARNINGS" 37 // DirectiveIgnoreMaxPayloadSize skips payload size validation when set. 38 DirectiveIgnoreMaxPayloadSize = "IGNORE_MAX_PAYLOAD_SIZE" 39 // DirectiveIgnoreMaxMemoryRows skips memory row validation when set. 40 DirectiveIgnoreMaxMemoryRows = "IGNORE_MAX_MEMORY_ROWS" 41 // DirectiveAllowScatter lets scatter plans pass through even when they are turned off by `no-scatter`. 42 DirectiveAllowScatter = "ALLOW_SCATTER" 43 // DirectiveAllowHashJoin lets the planner use hash join if possible 44 DirectiveAllowHashJoin = "ALLOW_HASH_JOIN" 45 // DirectiveQueryPlanner lets the user specify per query which planner should be used 46 DirectiveQueryPlanner = "PLANNER" 47 // DirectiveVExplainRunDMLQueries tells vexplain queries/all that it is okay to also run the query. 48 DirectiveVExplainRunDMLQueries = "EXECUTE_DML_QUERIES" 49 // DirectiveConsolidator enables the query consolidator. 50 DirectiveConsolidator = "CONSOLIDATOR" 51 ) 52 53 func isNonSpace(r rune) bool { 54 return !unicode.IsSpace(r) 55 } 56 57 // leadingCommentEnd returns the first index after all leading comments, or 58 // 0 if there are no leading comments. 59 func leadingCommentEnd(text string) (end int) { 60 hasComment := false 61 pos := 0 62 for pos < len(text) { 63 // Eat up any whitespace. Trailing whitespace will be considered part of 64 // the leading comments. 65 nextVisibleOffset := strings.IndexFunc(text[pos:], isNonSpace) 66 if nextVisibleOffset < 0 { 67 break 68 } 69 pos += nextVisibleOffset 70 remainingText := text[pos:] 71 72 // Found visible characters. Look for '/*' at the beginning 73 // and '*/' somewhere after that. 74 if len(remainingText) < 4 || remainingText[:2] != "/*" || remainingText[2] == '!' { 75 break 76 } 77 commentLength := 4 + strings.Index(remainingText[2:], "*/") 78 if commentLength < 4 { 79 // Missing end comment :/ 80 break 81 } 82 83 hasComment = true 84 pos += commentLength 85 } 86 87 if hasComment { 88 return pos 89 } 90 return 0 91 } 92 93 // trailingCommentStart returns the first index of trailing comments. 94 // If there are no trailing comments, returns the length of the input string. 95 func trailingCommentStart(text string) (start int) { 96 hasComment := false 97 reducedLen := len(text) 98 for reducedLen > 0 { 99 // Eat up any whitespace. Leading whitespace will be considered part of 100 // the trailing comments. 101 nextReducedLen := strings.LastIndexFunc(text[:reducedLen], isNonSpace) + 1 102 if nextReducedLen == 0 { 103 break 104 } 105 reducedLen = nextReducedLen 106 if reducedLen < 4 || text[reducedLen-2:reducedLen] != "*/" { 107 break 108 } 109 110 // Find the beginning of the comment 111 startCommentPos := strings.LastIndex(text[:reducedLen-2], "/*") 112 if startCommentPos < 0 || text[startCommentPos+2] == '!' { 113 // Badly formatted sql, or a special /*! comment 114 break 115 } 116 117 hasComment = true 118 reducedLen = startCommentPos 119 } 120 121 if hasComment { 122 return reducedLen 123 } 124 return len(text) 125 } 126 127 // MarginComments holds the leading and trailing comments that surround a query. 128 type MarginComments struct { 129 Leading string 130 Trailing string 131 } 132 133 // SplitMarginComments pulls out any leading or trailing comments from a raw sql query. 134 // This function also trims leading (if there's a comment) and trailing whitespace. 135 func SplitMarginComments(sql string) (query string, comments MarginComments) { 136 trailingStart := trailingCommentStart(sql) 137 leadingEnd := leadingCommentEnd(sql[:trailingStart]) 138 comments = MarginComments{ 139 Leading: strings.TrimLeftFunc(sql[:leadingEnd], unicode.IsSpace), 140 Trailing: strings.TrimRightFunc(sql[trailingStart:], unicode.IsSpace), 141 } 142 return strings.TrimFunc(sql[leadingEnd:trailingStart], func(c rune) bool { 143 return unicode.IsSpace(c) || c == ';' 144 }), comments 145 } 146 147 // StripLeadingComments trims the SQL string and removes any leading comments 148 func StripLeadingComments(sql string) string { 149 sql = strings.TrimFunc(sql, unicode.IsSpace) 150 151 for hasCommentPrefix(sql) { 152 switch sql[0] { 153 case '/': 154 // Multi line comment 155 index := strings.Index(sql, "*/") 156 if index <= 1 { 157 return sql 158 } 159 // don't strip /*! ... */ or /*!50700 ... */ 160 if len(sql) > 2 && sql[2] == '!' { 161 return sql 162 } 163 sql = sql[index+2:] 164 case '-': 165 // Single line comment 166 index := strings.Index(sql, "\n") 167 if index == -1 { 168 return "" 169 } 170 sql = sql[index+1:] 171 } 172 173 sql = strings.TrimFunc(sql, unicode.IsSpace) 174 } 175 176 return sql 177 } 178 179 func hasCommentPrefix(sql string) bool { 180 return len(sql) > 1 && ((sql[0] == '/' && sql[1] == '*') || (sql[0] == '-' && sql[1] == '-')) 181 } 182 183 // ExtractMysqlComment extracts the version and SQL from a comment-only query 184 // such as /*!50708 sql here */ 185 func ExtractMysqlComment(sql string) (string, string) { 186 sql = sql[3 : len(sql)-2] 187 188 digitCount := 0 189 endOfVersionIndex := strings.IndexFunc(sql, func(c rune) bool { 190 digitCount++ 191 return !unicode.IsDigit(c) || digitCount == 6 192 }) 193 if endOfVersionIndex < 0 { 194 return "", "" 195 } 196 if endOfVersionIndex < 5 { 197 endOfVersionIndex = 0 198 } 199 version := sql[0:endOfVersionIndex] 200 innerSQL := strings.TrimFunc(sql[endOfVersionIndex:], unicode.IsSpace) 201 202 return version, innerSQL 203 } 204 205 const commentDirectivePreamble = "/*vt+" 206 207 // CommentDirectives is the parsed representation for execution directives 208 // conveyed in query comments 209 type CommentDirectives struct { 210 m map[string]string 211 } 212 213 // Directives parses the comment list for any execution directives 214 // of the form: 215 // 216 // /*vt+ OPTION_ONE=1 OPTION_TWO OPTION_THREE=abcd */ 217 // 218 // It returns the map of the directive values or nil if there aren't any. 219 func (c *ParsedComments) Directives() *CommentDirectives { 220 if c == nil { 221 return nil 222 } 223 if c._directives == nil { 224 c._directives = &CommentDirectives{m: make(map[string]string)} 225 226 for _, commentStr := range c.comments { 227 if commentStr[0:5] != commentDirectivePreamble { 228 continue 229 } 230 231 // Split on whitespace and ignore the first and last directive 232 // since they contain the comment start/end 233 directives := strings.Fields(commentStr) 234 for i := 1; i < len(directives)-1; i++ { 235 directive, val, ok := strings.Cut(directives[i], "=") 236 if !ok { 237 val = "true" 238 } 239 c._directives.m[strings.ToLower(directive)] = val 240 } 241 } 242 } 243 return c._directives 244 } 245 246 func (c *ParsedComments) Length() int { 247 if c == nil { 248 return 0 249 } 250 return len(c.comments) 251 } 252 253 func (c *ParsedComments) Prepend(comment string) Comments { 254 if c == nil { 255 return Comments{comment} 256 } 257 comments := make(Comments, 0, len(c.comments)+1) 258 comments = append(comments, comment) 259 comments = append(comments, c.comments...) 260 return comments 261 } 262 263 // IsSet checks the directive map for the named directive and returns 264 // true if the directive is set and has a true/false or 0/1 value 265 func (d *CommentDirectives) IsSet(key string) bool { 266 if d == nil { 267 return false 268 } 269 val, found := d.m[strings.ToLower(key)] 270 if !found { 271 return false 272 } 273 // ParseBool handles "0", "1", "true", "false" and all similars 274 set, _ := strconv.ParseBool(val) 275 return set 276 } 277 278 // GetString gets a directive value as string, with default value if not found 279 func (d *CommentDirectives) GetString(key string, defaultVal string) (string, bool) { 280 if d == nil { 281 return "", false 282 } 283 val, ok := d.m[strings.ToLower(key)] 284 if !ok { 285 return defaultVal, false 286 } 287 if unquoted, err := strconv.Unquote(val); err == nil { 288 return unquoted, true 289 } 290 return val, true 291 } 292 293 // MultiShardAutocommitDirective returns true if multishard autocommit directive is set to true in query. 294 func MultiShardAutocommitDirective(stmt Statement) bool { 295 var comments *ParsedComments 296 switch stmt := stmt.(type) { 297 case *Insert: 298 comments = stmt.Comments 299 case *Update: 300 comments = stmt.Comments 301 case *Delete: 302 comments = stmt.Comments 303 } 304 return comments != nil && comments.Directives().IsSet(DirectiveMultiShardAutocommit) 305 } 306 307 // SkipQueryPlanCacheDirective returns true if skip query plan cache directive is set to true in query. 308 func SkipQueryPlanCacheDirective(stmt Statement) bool { 309 var comments *ParsedComments 310 switch stmt := stmt.(type) { 311 case *Select: 312 comments = stmt.Comments 313 case *Insert: 314 comments = stmt.Comments 315 case *Update: 316 comments = stmt.Comments 317 case *Delete: 318 comments = stmt.Comments 319 } 320 return comments != nil && comments.Directives().IsSet(DirectiveSkipQueryPlanCache) 321 } 322 323 // IgnoreMaxPayloadSizeDirective returns true if the max payload size override 324 // directive is set to true. 325 func IgnoreMaxPayloadSizeDirective(stmt Statement) bool { 326 var comments *ParsedComments 327 switch stmt := stmt.(type) { 328 // For transactional statements, they should always be passed down and 329 // should not come into max payload size requirement. 330 case *Begin, *Commit, *Rollback, *Savepoint, *SRollback, *Release: 331 return true 332 case *Select: 333 comments = stmt.Comments 334 case *Insert: 335 comments = stmt.Comments 336 case *Update: 337 comments = stmt.Comments 338 case *Delete: 339 comments = stmt.Comments 340 } 341 return comments != nil && comments.Directives().IsSet(DirectiveIgnoreMaxPayloadSize) 342 } 343 344 // IgnoreMaxMaxMemoryRowsDirective returns true if the max memory rows override 345 // directive is set to true. 346 func IgnoreMaxMaxMemoryRowsDirective(stmt Statement) bool { 347 var comments *ParsedComments 348 switch stmt := stmt.(type) { 349 case *Select: 350 comments = stmt.Comments 351 case *Insert: 352 comments = stmt.Comments 353 case *Update: 354 comments = stmt.Comments 355 case *Delete: 356 comments = stmt.Comments 357 } 358 return comments != nil && comments.Directives().IsSet(DirectiveIgnoreMaxMemoryRows) 359 } 360 361 // AllowScatterDirective returns true if the allow scatter override is set to true 362 func AllowScatterDirective(stmt Statement) bool { 363 var comments *ParsedComments 364 switch stmt := stmt.(type) { 365 case *Select: 366 comments = stmt.Comments 367 case *Insert: 368 comments = stmt.Comments 369 case *Update: 370 comments = stmt.Comments 371 case *Delete: 372 comments = stmt.Comments 373 } 374 return comments != nil && comments.Directives().IsSet(DirectiveAllowScatter) 375 } 376 377 // Consolidator returns the consolidator option. 378 func Consolidator(stmt Statement) querypb.ExecuteOptions_Consolidator { 379 var comments *ParsedComments 380 switch stmt := stmt.(type) { 381 case *Select: 382 comments = stmt.Comments 383 default: 384 return querypb.ExecuteOptions_CONSOLIDATOR_UNSPECIFIED 385 } 386 if comments == nil { 387 return querypb.ExecuteOptions_CONSOLIDATOR_UNSPECIFIED 388 } 389 directives := comments.Directives() 390 strv, isSet := directives.GetString(DirectiveConsolidator, "") 391 if !isSet { 392 return querypb.ExecuteOptions_CONSOLIDATOR_UNSPECIFIED 393 } 394 if i32v, ok := querypb.ExecuteOptions_Consolidator_value["CONSOLIDATOR_"+strings.ToUpper(strv)]; ok { 395 return querypb.ExecuteOptions_Consolidator(i32v) 396 } 397 return querypb.ExecuteOptions_CONSOLIDATOR_UNSPECIFIED 398 }