|
| 1 | +// This file uses es6 features and is loaded optimistically. |
| 2 | + |
| 3 | +const SqlString = require('../SqlString'); |
| 4 | +const { |
| 5 | + calledAsTemplateTagQuick, |
| 6 | + memoizedTagFunction, |
| 7 | + trimCommonWhitespaceFromLines, |
| 8 | + TypedString |
| 9 | +} = require('template-tag-common'); |
| 10 | + |
| 11 | +// A simple lexer for SQL. |
| 12 | +// SQL has many divergent dialects with subtly different |
| 13 | +// conventions for string escaping and comments. |
| 14 | +// This just attempts to roughly tokenize MySQL's specific variant. |
| 15 | +// See also |
| 16 | +// https://www.w3.org/2005/05/22-SPARQL-MySQL/sql_yacc |
| 17 | +// https://github.com/twitter/mysql/blob/master/sql/sql_lex.cc |
| 18 | +// https://dev.mysql.com/doc/refman/5.7/en/string-literals.html |
| 19 | + |
| 20 | +// "--" followed by whitespace starts a line comment |
| 21 | +// "#" |
| 22 | +// "/*" starts an inline comment ended at first "*/" |
| 23 | +// \N means null |
| 24 | +// Prefixed strings x'...' is a hex string, b'...' is a binary string, .... |
| 25 | +// '...', "..." are strings. `...` escapes identifiers. |
| 26 | +// doubled delimiters and backslash both escape |
| 27 | +// doubled delimiters work in `...` identifiers |
| 28 | + |
| 29 | +const PREFIX_BEFORE_DELIMITER = new RegExp( |
| 30 | + '^(?:' + |
| 31 | + ( |
| 32 | + // Comment |
| 33 | + '--(?=[\\t\\r\\n ])[^\\r\\n]*' + |
| 34 | + '|#[^\\r\\n]*' + |
| 35 | + '|/[*][\\s\\S]*?[*]/' |
| 36 | + ) + |
| 37 | + '|' + |
| 38 | + ( |
| 39 | + // Run of non-comment non-string starts |
| 40 | + '(?:[^\'"`\\-/#]|-(?!-)|/(?![*]))' |
| 41 | + ) + |
| 42 | + ')*'); |
| 43 | +const DELIMITED_BODIES = { |
| 44 | + '\'' : /^(?:[^'\\]|\\[\s\S]|'')*/, |
| 45 | + '"' : /^(?:[^"\\]|\\[\s\S]|"")*/, |
| 46 | + '`' : /^(?:[^`\\]|\\[\s\S]|``)*/ |
| 47 | +}; |
| 48 | + |
| 49 | +/** |
| 50 | + * Template tag that creates a new Error with a message. |
| 51 | + * @param {!Array.<string>} strs a valid TemplateObject. |
| 52 | + * @return {string} A message suitable for the Error constructor. |
| 53 | + */ |
| 54 | +function msg (strs, ...dyn) { |
| 55 | + let message = String(strs[0]); |
| 56 | + for (let i = 0; i < dyn.length; ++i) { |
| 57 | + message += JSON.stringify(dyn[i]) + strs[i + 1]; |
| 58 | + } |
| 59 | + return message; |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Returns a function that can be fed chunks of input and which |
| 64 | + * returns a delimiter context. |
| 65 | + * |
| 66 | + * @return {!function (string) : string} |
| 67 | + * a stateful function that takes a string of SQL text and |
| 68 | + * returns the context after it. Subsequent calls will assume |
| 69 | + * that context. |
| 70 | + */ |
| 71 | +function makeLexer () { |
| 72 | + let errorMessage = null; |
| 73 | + let delimiter = null; |
| 74 | + return (text) => { |
| 75 | + if (errorMessage) { |
| 76 | + // Replay the error message if we've already failed. |
| 77 | + throw new Error(errorMessage); |
| 78 | + } |
| 79 | + text = String(text); |
| 80 | + while (text) { |
| 81 | + const pattern = delimiter |
| 82 | + ? DELIMITED_BODIES[delimiter] |
| 83 | + : PREFIX_BEFORE_DELIMITER; |
| 84 | + const match = pattern.exec(text); |
| 85 | + if (!match) { |
| 86 | + throw new Error( |
| 87 | + errorMessage = msg`Failed to lex starting at ${text}`); |
| 88 | + } |
| 89 | + let nConsumed = match[0].length; |
| 90 | + if (text.length > nConsumed) { |
| 91 | + const chr = text.charAt(nConsumed); |
| 92 | + if (delimiter) { |
| 93 | + if (chr === delimiter) { |
| 94 | + delimiter = null; |
| 95 | + ++nConsumed; |
| 96 | + } else { |
| 97 | + throw new Error( |
| 98 | + errorMessage = msg`Expected ${chr} at ${text}`); |
| 99 | + } |
| 100 | + } else if (Object.hasOwnProperty.call(DELIMITED_BODIES, chr)) { |
| 101 | + delimiter = chr; |
| 102 | + ++nConsumed; |
| 103 | + } else { |
| 104 | + throw new Error( |
| 105 | + errorMessage = msg`Expected delimiter at ${text}`); |
| 106 | + } |
| 107 | + } |
| 108 | + text = text.substring(nConsumed); |
| 109 | + } |
| 110 | + return delimiter; |
| 111 | + }; |
| 112 | +} |
| 113 | + |
| 114 | +/** A string wrapper that marks its content as a SQL identifier. */ |
| 115 | +class Identifier extends TypedString {} |
| 116 | + |
| 117 | +/** |
| 118 | + * A string wrapper that marks its content as a series of |
| 119 | + * well-formed SQL tokens. |
| 120 | + */ |
| 121 | +class SqlFragment extends TypedString {} |
| 122 | + |
| 123 | +/** |
| 124 | + * Analyzes the static parts of the tag content. |
| 125 | + * |
| 126 | + * @param {!Array.<string>} strings a valid TemplateObject. |
| 127 | + * @return { !{ |
| 128 | + * raw: !Array.<string>, |
| 129 | + * delimiters : !Array.<string>, |
| 130 | + * chunks: !Array.<string> |
| 131 | + * } } |
| 132 | + * A record like { raw, delimiters, chunks } |
| 133 | + * where delimiter is a contextual cue and chunk is |
| 134 | + * the adjusted raw text. |
| 135 | + */ |
| 136 | +function computeStatic (strings) { |
| 137 | + const { raw } = trimCommonWhitespaceFromLines(strings); |
| 138 | + |
| 139 | + const delimiters = []; |
| 140 | + const chunks = []; |
| 141 | + |
| 142 | + const lexer = makeLexer(); |
| 143 | + |
| 144 | + let delimiter = null; |
| 145 | + for (let i = 0, len = raw.length; i < len; ++i) { |
| 146 | + let chunk = String(raw[i]); |
| 147 | + if (delimiter === '`') { |
| 148 | + // Treat raw \` in an identifier literal as an ending delimiter. |
| 149 | + chunk = chunk.replace(/^([^\\`]|\\[\s\S])*\\`/, '$1`'); |
| 150 | + } |
| 151 | + const newDelimiter = lexer(chunk); |
| 152 | + if (newDelimiter === '`' && !delimiter) { |
| 153 | + // Treat literal \` outside a string context as starting an |
| 154 | + // identifier literal |
| 155 | + chunk = chunk.replace( |
| 156 | + /((?:^|[^\\])(?:\\\\)*)\\(`(?:[^`\\]|\\[\s\S])*)$/, '$1$2'); |
| 157 | + } |
| 158 | + |
| 159 | + chunks.push(chunk); |
| 160 | + delimiters.push(newDelimiter); |
| 161 | + delimiter = newDelimiter; |
| 162 | + } |
| 163 | + |
| 164 | + if (delimiter) { |
| 165 | + throw new Error(`Unclosed quoted string: ${delimiter}`); |
| 166 | + } |
| 167 | + |
| 168 | + return { raw, delimiters, chunks }; |
| 169 | +} |
| 170 | + |
| 171 | +function interpolateSqlIntoFragment ( |
| 172 | + { raw, delimiters, chunks }, strings, values) { |
| 173 | + // A buffer to accumulate output. |
| 174 | + let [ result ] = chunks; |
| 175 | + for (let i = 1, len = raw.length; i < len; ++i) { |
| 176 | + const chunk = chunks[i]; |
| 177 | + // The count of values must be 1 less than the surrounding |
| 178 | + // chunks of literal text. |
| 179 | + if (i !== 0) { |
| 180 | + const delimiter = delimiters[i - 1]; |
| 181 | + const value = values[i - 1]; |
| 182 | + if (delimiter) { |
| 183 | + result += escapeDelimitedValue(value, delimiter); |
| 184 | + } else { |
| 185 | + result = appendValue(result, value, chunk); |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + result += chunk; |
| 190 | + } |
| 191 | + |
| 192 | + return new SqlFragment(result); |
| 193 | +} |
| 194 | + |
| 195 | +function escapeDelimitedValue (value, delimiter) { |
| 196 | + if (delimiter === '`') { |
| 197 | + return SqlString.escapeId(String(value)).replace(/^`|`$/g, ''); |
| 198 | + } |
| 199 | + const escaped = SqlString.escape(String(value)); |
| 200 | + return escaped.substring(1, escaped.length - 1); |
| 201 | +} |
| 202 | + |
| 203 | +function appendValue (resultBefore, value, chunk) { |
| 204 | + let needsSpace = false; |
| 205 | + let result = resultBefore; |
| 206 | + const valueArray = Array.isArray(value) ? value : [ value ]; |
| 207 | + for (let i = 0, nValues = valueArray.length; i < nValues; ++i) { |
| 208 | + if (i) { |
| 209 | + result += ', '; |
| 210 | + } |
| 211 | + |
| 212 | + const one = valueArray[i]; |
| 213 | + let valueStr = null; |
| 214 | + if (one instanceof SqlFragment) { |
| 215 | + if (!/(?:^|[\n\r\t ,\x28])$/.test(result)) { |
| 216 | + result += ' '; |
| 217 | + } |
| 218 | + valueStr = one.toString(); |
| 219 | + needsSpace = i + 1 === nValues; |
| 220 | + } else if (one instanceof Identifier) { |
| 221 | + valueStr = SqlString.escapeId(one.toString()); |
| 222 | + } else { |
| 223 | + // If we need to handle nested arrays, we would recurse here. |
| 224 | + valueStr = SqlString.format('?', one); |
| 225 | + } |
| 226 | + result += valueStr; |
| 227 | + } |
| 228 | + |
| 229 | + if (needsSpace && chunk && !/^[\n\r\t ,\x29]/.test(chunk)) { |
| 230 | + result += ' '; |
| 231 | + } |
| 232 | + |
| 233 | + return result; |
| 234 | +} |
| 235 | + |
| 236 | +/** |
| 237 | + * Template tag function that contextually autoescapes values |
| 238 | + * producing a SqlFragment. |
| 239 | + */ |
| 240 | +const sql = memoizedTagFunction(computeStatic, interpolateSqlIntoFragment); |
| 241 | +sql.Identifier = Identifier; |
| 242 | +sql.Fragment = SqlFragment; |
| 243 | +sql.calledAsTemplateTagQuick = calledAsTemplateTagQuick; |
| 244 | + |
| 245 | +if (require('process').env.npm_lifecycle_event === 'test') { |
| 246 | + // Expose for testing. |
| 247 | + // Harmless if this leaks |
| 248 | + sql.makeLexer = makeLexer; |
| 249 | +} |
| 250 | + |
| 251 | +module.exports = sql; |
0 commit comments