go.temporal.io/server@v1.23.0/common/persistence/query_util.go (about) 1 // The MIT License 2 // 3 // Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. 4 // 5 // Copyright (c) 2020 Uber Technologies, Inc. 6 // 7 // Permission is hereby granted, free of charge, to any person obtaining a copy 8 // of this software and associated documentation files (the "Software"), to deal 9 // in the Software without restriction, including without limitation the rights 10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 // copies of the Software, and to permit persons to whom the Software is 12 // furnished to do so, subject to the following conditions: 13 // 14 // The above copyright notice and this permission notice shall be included in 15 // all copies or substantial portions of the Software. 16 // 17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 // THE SOFTWARE. 24 25 package persistence 26 27 import ( 28 "bytes" 29 "fmt" 30 "io" 31 "os" 32 "strings" 33 "unicode" 34 ) 35 36 const ( 37 queryDelimiter = ';' 38 querySliceDefaultSize = 100 39 40 sqlLeftParenthesis = '(' 41 sqlRightParenthesis = ')' 42 sqlBeginKeyword = "begin" 43 sqlEndKeyword = "end" 44 sqlLineComment = "--" 45 sqlSingleQuote = '\'' 46 sqlDoubleQuote = '"' 47 ) 48 49 // LoadAndSplitQuery loads and split cql / sql query into one statement per string. 50 // Comments are removed from the query. 51 func LoadAndSplitQuery( 52 filePaths []string, 53 ) ([]string, error) { 54 var files []io.Reader 55 56 for _, filePath := range filePaths { 57 f, err := os.Open(filePath) 58 if err != nil { 59 return nil, fmt.Errorf("error opening file %s: %w", filePath, err) 60 } 61 files = append(files, f) 62 } 63 64 return LoadAndSplitQueryFromReaders(files) 65 } 66 67 // LoadAndSplitQueryFromReaders loads and split cql / sql query into one statement per string. 68 // Comments are removed from the query. 69 func LoadAndSplitQueryFromReaders( 70 readers []io.Reader, 71 ) ([]string, error) { 72 result := make([]string, 0, querySliceDefaultSize) 73 for _, r := range readers { 74 content, err := io.ReadAll(r) 75 if err != nil { 76 return nil, fmt.Errorf("error reading contents: %w", err) 77 } 78 n := len(content) 79 contentStr := string(bytes.ToLower(content)) 80 for i, j := 0, 0; i < n; i = j { 81 // stack to keep track of open parenthesis/blocks 82 var st []byte 83 var stmtBuilder strings.Builder 84 85 stmtLoop: 86 for ; j < n; j++ { 87 switch contentStr[j] { 88 case queryDelimiter: 89 if len(st) == 0 { 90 j++ 91 break stmtLoop 92 } 93 94 case sqlLeftParenthesis: 95 st = append(st, sqlLeftParenthesis) 96 97 case sqlRightParenthesis: 98 if len(st) == 0 || st[len(st)-1] != sqlLeftParenthesis { 99 return nil, fmt.Errorf("error reading contents: unmatched right parenthesis") 100 } 101 st = st[:len(st)-1] 102 103 case sqlBeginKeyword[0]: 104 if hasWordAt(contentStr, sqlBeginKeyword, j) { 105 st = append(st, sqlBeginKeyword[0]) 106 j += len(sqlBeginKeyword) - 1 107 } 108 109 case sqlEndKeyword[0]: 110 if hasWordAt(contentStr, sqlEndKeyword, j) { 111 if len(st) == 0 || st[len(st)-1] != sqlBeginKeyword[0] { 112 return nil, fmt.Errorf("error reading contents: unmatched `END` keyword") 113 } 114 st = st[:len(st)-1] 115 j += len(sqlEndKeyword) - 1 116 } 117 118 case sqlSingleQuote, sqlDoubleQuote: 119 quote := contentStr[j] 120 j++ 121 for j < n && contentStr[j] != quote { 122 j++ 123 } 124 if j == n { 125 return nil, fmt.Errorf("error reading contents: unmatched quotes") 126 } 127 128 case sqlLineComment[0]: 129 if j+len(sqlLineComment) <= n && contentStr[j:j+len(sqlLineComment)] == sqlLineComment { 130 _, _ = stmtBuilder.Write(bytes.TrimRight(content[i:j], " ")) 131 for j < n && contentStr[j] != '\n' { 132 j++ 133 } 134 i = j 135 } 136 137 default: 138 // no-op: generic character 139 } 140 } 141 142 if len(st) > 0 { 143 switch st[len(st)-1] { 144 case sqlLeftParenthesis: 145 return nil, fmt.Errorf("error reading contents: unmatched left parenthesis") 146 case sqlBeginKeyword[0]: 147 return nil, fmt.Errorf("error reading contents: unmatched `BEGIN` keyword") 148 default: 149 // should never enter here 150 return nil, fmt.Errorf("error reading contents: unmatched `%c`", st[len(st)-1]) 151 } 152 } 153 154 _, _ = stmtBuilder.Write(content[i:j]) 155 stmt := strings.TrimSpace(stmtBuilder.String()) 156 if stmt == "" { 157 continue 158 } 159 result = append(result, stmt) 160 } 161 } 162 return result, nil 163 } 164 165 // hasWordAt is a simple test to check if it matches the whole word: 166 // it checks if the adjacent charactes are not alphanumeric if they exist. 167 func hasWordAt(s, word string, pos int) bool { 168 if pos+len(word) > len(s) || s[pos:pos+len(word)] != word { 169 return false 170 } 171 if pos > 0 && isAlphanumeric(s[pos-1]) { 172 return false 173 } 174 if pos+len(word) < len(s) && isAlphanumeric(s[pos+len(word)]) { 175 return false 176 } 177 return true 178 } 179 180 func isAlphanumeric(c byte) bool { 181 return unicode.IsLetter(rune(c)) || unicode.IsDigit(rune(c)) 182 }