├── .gitignore ├── API.md ├── LICENSE ├── README.md ├── SqlWhereParser.js ├── gulpfile.js ├── package.json ├── sql-where-parser.min.js └── test └── SqlWhereParser.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | - [#parse(sql:String):Object](#sqlwhereparser-api-methods-parsesqlstringobject) 3 | - [#parse(sql:String, evaluator:Function):*](#sqlwhereparser-api-methods-parsesqlstring-evaluatorfunction) 4 | - [#toArray(sql:String):Array](#sqlwhereparser-api-methods-toarraysqlstringarray) 5 | - [#operatorPrecedenceFromValues(operatorValue1:String|Symbol, operatorValue2:String|Symbol):Boolean](#sqlwhereparser-api-methods-operatorprecedencefromvaluesoperatorvalue1stringsymbol-operatorvalue2stringsymbolboolean) 6 | - [#getOperator(operatorValue:String|Symbol):Operator](#sqlwhereparser-api-methods-getoperatoroperatorvaluestringsymboloperator) 7 | - [#defaultEvaluator(operatorValue:String|Symbol, operands:Array)](#sqlwhereparser-api-methods-defaultevaluatoroperatorvaluestringsymbol-operandsarray) 8 | - [#tokenizer:TokenizeThis](#sqlwhereparser-api-methods-tokenizertokenizethis) 9 | - [#operators:Object](#sqlwhereparser-api-methods-operatorsobject) 10 | - [.defaultConfig:Object](#sqlwhereparser-api-methods-defaultconfigobject) 11 | - [.Operator:Operator](#sqlwhereparser-api-methods-operatoroperator) 12 | - [.OPERATOR_UNARY_MINUS:Symbol](#sqlwhereparser-api-methods-operator_unary_minussymbol) 13 | 14 | 15 | #### #parse(sql:String):Object 16 | 17 | Parses the SQL string into an AST with the proper order of operations. 18 | 19 | ```js 20 | const sql = 'name = "Shaun Persad" AND job = developer AND (gender = male OR type = person AND location IN (NY, America) AND hobby = coding)'; 21 | const parser = new SqlWhereParser(); 22 | const parsed = parser.parse(sql); 23 | /** 24 | * Original. 25 | */ 26 | //'name = "Shaun Persad" AND job = developer AND (gender = male OR type = person AND location IN (NY, America) AND hobby = coding)'; 27 | /** 28 | * Perform equals. 29 | */ 30 | //'(name = "Shaun Persad") AND (job = developer) AND ((gender = male) OR (type = person) AND location IN (NY, America) AND (hobby = coding))'; 31 | /** 32 | * Perform IN 33 | */ 34 | //'(name = "Shaun Persad") AND (job = developer) AND ((gender = male) OR (type = person) AND (location IN (NY, America)) AND (hobby = coding))'; 35 | /** 36 | * Perform AND 37 | */ 38 | //'(((name = "Shaun Persad") AND (job = developer)) AND ((gender = male) OR (((type = person) AND (location IN (NY, America))) AND (hobby = coding))))'; 39 | equals(parsed, { 40 | 'AND': [ 41 | { 42 | 'AND': [ 43 | { 44 | '=': ['name', 'Shaun Persad'] 45 | }, 46 | { 47 | '=': ['job', 'developer'] 48 | } 49 | ] 50 | }, 51 | { 52 | 'OR': [ 53 | { 54 | '=': ['gender', 'male'] 55 | }, 56 | { 57 | 'AND': [ 58 | { 59 | 'AND': [ 60 | { 61 | '=': ['type', 'person'] 62 | }, 63 | { 64 | 'IN': ['location', ['NY', 'America']] 65 | } 66 | ] 67 | }, 68 | { 69 | '=': ['hobby', 'coding'] 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | ] 76 | }); 77 | ``` 78 | 79 | It handles the unary minus case appropriately. 80 | 81 | ```js 82 | const parser = new SqlWhereParser(); 83 | let parsed = parser.parse('1 + -5'); 84 | equals(parsed, { 85 | '+': [ 86 | 1, 87 | { 88 | '-': [5] 89 | } 90 | ] 91 | }); 92 | parsed = parser.parse('-1 + -(5 - - 5)'); 93 | equals(parsed, { 94 | '+': [ 95 | { 96 | '-': [1] 97 | }, 98 | { 99 | '-': [ 100 | { 101 | '-': [ 102 | 5, 103 | { 104 | '-': [5] 105 | } 106 | ] 107 | } 108 | ] 109 | } 110 | ] 111 | }); 112 | ``` 113 | 114 | It handles the BETWEEN case appropriately. 115 | 116 | ```js 117 | const parser = new SqlWhereParser(); 118 | const parsed = parser.parse('A BETWEEN 5 AND 10 AND B = C'); 119 | equals(parsed, { 120 | 'AND': [ 121 | { 122 | 'BETWEEN': ['A', 5, 10] 123 | }, 124 | { 125 | '=': ['B', 'C'] 126 | } 127 | ] 128 | }); 129 | ``` 130 | 131 | Unnecessarily nested parentheses do not matter. 132 | 133 | ```js 134 | const sql = '((((name = "Shaun Persad")) AND (age >= 27)))'; 135 | const parser = new SqlWhereParser(); 136 | const parsed = parser.parse(sql); 137 | equals(parsed, { 138 | 'AND': [ 139 | { 140 | '=': ['name', 'Shaun Persad' 141 | ] 142 | }, 143 | { 144 | '>=': ['age', 27] 145 | } 146 | ] 147 | }); 148 | ``` 149 | 150 | Throws a SyntaxError if the supplied parentheses do not match. 151 | 152 | ```js 153 | const sql = '(name = "Shaun Persad" AND age >= 27'; 154 | const parser = new SqlWhereParser(); 155 | let failed = false; 156 | try { 157 | const parsed = parser.parse(sql); 158 | failed = false; 159 | } catch (e) { 160 | failed = (e.constructor === SyntaxError); 161 | } 162 | if (!failed) { 163 | throw new Error('A SyntaxError was not thrown.'); 164 | } 165 | ``` 166 | 167 | 168 | 169 | #### #parse(sql:String, evaluator:Function):* 170 | 171 | Uses the supplied `evaluator(operatorValue:String|Symbol, operands:Array)` function to convert an operator and its operands into its evaluation. The default evaluator actually does no "evaluation" in the mathematical sense. Instead it creates an object whose key is the operator, and the value is an array of the operands. 172 | 173 | ```js 174 | let timesCalled = 0; 175 | const sql = 'name = "Shaun Persad" AND age >= 27'; 176 | const parser = new SqlWhereParser(); 177 | const parsed = parser.parse(sql, (operatorValue, operands) => { 178 | timesCalled++; 179 | return parser.defaultEvaluator(operatorValue, operands); 180 | }); 181 | timesCalled.should.equal(3); 182 | equals(parsed, { 183 | 'AND': [ 184 | { 185 | '=': ['name', 'Shaun Persad' 186 | ] 187 | }, 188 | { 189 | '>=': ['age', 27] 190 | } 191 | ] 192 | }); 193 | ``` 194 | 195 | "Evaluation" is subjective, and this can be exploited to convert the default object-based structure of the AST into something else, like this array-based structure. 196 | 197 | ```js 198 | const sql = 'name = "Shaun Persad" AND age >= 27'; 199 | const parser = new SqlWhereParser(); 200 | const parsed = parser.parse(sql, (operatorValue, operands) => { 201 | return [operatorValue, operands]; 202 | }); 203 | equals(parsed, [ 204 | 'AND', 205 | [ 206 | ['=',['name', 'Shaun Persad']], 207 | ['>=', ['age', 27]] 208 | ] 209 | ]); 210 | ``` 211 | 212 | 213 | 214 | #### #toArray(sql:String):Array 215 | 216 | Parses the SQL string into a nested array, where each expression is its own array. 217 | 218 | ```js 219 | const sql = '(name = "Shaun Persad") AND (age >= (20 + 7))'; 220 | const parser = new SqlWhereParser(); 221 | const sqlArray = parser.toArray(sql); 222 | equals(sqlArray, [ 223 | ['name', '=', 'Shaun Persad'], 'AND', ['age', '>=', [20, '+', 7]] 224 | ]); 225 | ``` 226 | 227 | Unnecessarily nested parentheses do not matter. 228 | 229 | ```js 230 | const sql = '((((name = "Shaun Persad"))) AND ((age) >= ((20 + 7))))'; 231 | const parser = new SqlWhereParser(); 232 | const sqlArray = parser.toArray(sql); 233 | equals(sqlArray, [ 234 | ['name', '=', 'Shaun Persad'], 'AND', ['age', '>=', [20, '+', 7]] 235 | ]); 236 | ``` 237 | 238 | 239 | 240 | #### #operatorPrecedenceFromValues(operatorValue1:String|Symbol, operatorValue2:String|Symbol):Boolean 241 | 242 | Determines if operator 2 is of a higher precedence than operator 1. 243 | 244 | For full precedence list, check the [defaultConfig](#defaultconfigobject) object. 245 | 246 | ```js 247 | const parser = new SqlWhereParser(); 248 | parser.operatorPrecedenceFromValues('AND', 'OR').should.equal(false); // AND is higher than OR 249 | parser.operatorPrecedenceFromValues('+', '-').should.equal(true); // + and - are equal 250 | parser.operatorPrecedenceFromValues('+', '*').should.equal(true); // * is higher than + 251 | ``` 252 | 253 | It also works if either of the operator values are a Symbol instead of a String. 254 | 255 | ```js 256 | const parser = new SqlWhereParser(); 257 | parser.operatorPrecedenceFromValues(SqlWhereParser.OPERATOR_UNARY_MINUS, '-').should.equal(false); // unary minus is higher than minus 258 | ``` 259 | 260 | 261 | 262 | #### #getOperator(operatorValue:String|Symbol):Operator 263 | 264 | Returns the corresponding instance of the Operator class. 265 | 266 | ```js 267 | const parser = new SqlWhereParser(); 268 | const minus = parser.getOperator('-'); 269 | minus.should.be.instanceOf(SqlWhereParser.Operator); 270 | minus.should.have.property('value', '-'); 271 | minus.should.have.property('precedence', 4); 272 | minus.should.have.property('type', 2); // its binary 273 | ``` 274 | 275 | It also works if the operator value is a Symbol instead of a String. 276 | 277 | ```js 278 | const parser = new SqlWhereParser(); 279 | const unaryMinus = parser.getOperator(SqlWhereParser.OPERATOR_UNARY_MINUS); 280 | unaryMinus.should.be.instanceOf(SqlWhereParser.Operator); 281 | unaryMinus.should.have.property('value', SqlWhereParser.OPERATOR_UNARY_MINUS); 282 | unaryMinus.should.have.property('precedence', 1); 283 | unaryMinus.should.have.property('type', 1); // its unary 284 | ``` 285 | 286 | 287 | 288 | #### #defaultEvaluator(operatorValue:String|Symbol, operands:Array) 289 | 290 | Converts the operator and its operands into an object whose key is the operator value, and the value is the array of operands. 291 | 292 | ```js 293 | const parser = new SqlWhereParser(); 294 | const evaluation = parser.defaultEvaluator('OPERATOR', [1, 2, 3]); 295 | equals(evaluation, { 296 | 'OPERATOR': [1, 2, 3] 297 | }); 298 | ``` 299 | 300 | ...Except for the special "," operator, which acts like a binary operator, but is not really an operator. It combines anything comma-separated into an array. 301 | 302 | ```js 303 | const parser = new SqlWhereParser(); 304 | const evaluation = parser.defaultEvaluator(',', [1, 2]); 305 | equals(evaluation, [1, 2]); 306 | ``` 307 | 308 | When used in the recursive manner that it is, we are able to combine the results of several binary comma operations into a single array. 309 | 310 | ```js 311 | const parser = new SqlWhereParser(); 312 | const evaluation = parser.defaultEvaluator(',', [[1, 2], 3]); 313 | equals(evaluation, [1, 2, 3]); 314 | ``` 315 | 316 | With the unary minus Symbol, it converts it back into a regular minus string, since the operands have been determined by this point. 317 | 318 | ```js 319 | const parser = new SqlWhereParser(); 320 | const evaluation = parser.defaultEvaluator(SqlWhereParser.OPERATOR_UNARY_MINUS, [1]); 321 | equals(evaluation, { 322 | '-': [1] 323 | }); 324 | ``` 325 | 326 | 327 | 328 | #### #tokenizer:TokenizeThis 329 | 330 | The tokenizer used on the string. See documentation [here](https://github.com/shaunpersad/tokenize-this). 331 | 332 | ```js 333 | const parser = new SqlWhereParser(); 334 | parser.tokenizer.should.be.instanceOf(TokenizeThis); 335 | ``` 336 | 337 | 338 | 339 | #### #operators:Object 340 | 341 | An object whose keys are the supported operator values, and whose values are instances of the Operator class. 342 | 343 | ```js 344 | const parser = new SqlWhereParser(); 345 | const operators = ["!", SqlWhereParser.OPERATOR_UNARY_MINUS, "^","*","/","%","+","-","=","<",">","<=",">=","!=",",","NOT","BETWEEN","IN","IS","LIKE","AND","OR"]; 346 | operators.forEach((operator) => { 347 | parser.operators[operator].should.be.instanceOf(SqlWhereParser.Operator); 348 | }); 349 | ``` 350 | 351 | 352 | 353 | #### .defaultConfig:Object 354 | 355 | The default config object used when no config is supplied. For the tokenizer config options, see [here](https://github.com/shaunpersad/tokenize-this#defaultconfigobject). 356 | 357 | ```js 358 | const OPERATOR_TYPE_UNARY = 1; 359 | const OPERATOR_TYPE_BINARY = 2; 360 | const OPERATOR_TYPE_TERNARY = 3; 361 | const unaryMinusDefinition = { 362 | [SqlWhereParser.OPERATOR_UNARY_MINUS]: OPERATOR_TYPE_UNARY 363 | }; 364 | equals(SqlWhereParser.defaultConfig, { 365 | operators: [ 366 | { 367 | '!': OPERATOR_TYPE_UNARY 368 | }, 369 | unaryMinusDefinition, 370 | { 371 | '^': OPERATOR_TYPE_BINARY 372 | }, 373 | { 374 | '*': OPERATOR_TYPE_BINARY, 375 | '/': OPERATOR_TYPE_BINARY, 376 | '%': OPERATOR_TYPE_BINARY 377 | }, 378 | { 379 | '+': OPERATOR_TYPE_BINARY, 380 | '-': OPERATOR_TYPE_BINARY 381 | }, 382 | { 383 | '=': OPERATOR_TYPE_BINARY, 384 | '<': OPERATOR_TYPE_BINARY, 385 | '>': OPERATOR_TYPE_BINARY, 386 | '<=': OPERATOR_TYPE_BINARY, 387 | '>=': OPERATOR_TYPE_BINARY, 388 | '!=': OPERATOR_TYPE_BINARY 389 | }, 390 | { 391 | ',': OPERATOR_TYPE_BINARY // We treat commas as an operator, to aid in turning arbitrary numbers of comma-separated values into arrays. 392 | }, 393 | { 394 | 'NOT': OPERATOR_TYPE_UNARY 395 | }, 396 | { 397 | 'BETWEEN': OPERATOR_TYPE_TERNARY, 398 | 'IN': OPERATOR_TYPE_BINARY, 399 | 'IS': OPERATOR_TYPE_BINARY, 400 | 'LIKE': OPERATOR_TYPE_BINARY 401 | }, 402 | { 403 | 'AND': OPERATOR_TYPE_BINARY 404 | }, 405 | { 406 | 'OR': OPERATOR_TYPE_BINARY 407 | } 408 | ], 409 | tokenizer: { 410 | shouldTokenize: ['(', ')', ',', '*', '/', '%', '+', '-', '=', '!=','!', '<', '>', '<=', '>=', '^'], 411 | shouldMatch: ['"', "'", '`'], 412 | shouldDelimitBy: [' ', "\n", "\r", "\t"] 413 | } 414 | }); 415 | ``` 416 | 417 | 418 | 419 | #### .Operator:Operator 420 | 421 | The Operator class. 422 | 423 | ```js 424 | const parser = new SqlWhereParser(); 425 | parser.operators['AND'].should.be.instanceOf(SqlWhereParser.Operator); 426 | ``` 427 | 428 | 429 | 430 | #### .OPERATOR_UNARY_MINUS:Symbol 431 | 432 | The Symbol used as the unary minus operator value. 433 | 434 | ```js 435 | (typeof SqlWhereParser.OPERATOR_UNARY_MINUS).should.equal('symbol'); 436 | ``` 437 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 shaunpersad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SqlWhereParser 2 | 3 | ## What is it? 4 | 5 | SqlWhereParser parses the WHERE portion of an SQL-like string into an abstract syntax tree. 6 | 7 | ```js 8 | const sql = 'name = "Shaun Persad" AND age >= 27'; 9 | const parser = new SqlWhereParser(); 10 | const parsed = parser.parse(sql); 11 | /** 12 | * The tree is object-based, where each key is the operator, and its value is an array of the operands. 13 | * The number of operands depends on if the operation is defined as unary, binary, or ternary in the config. 14 | */ 15 | equals(parsed, { 16 | 'AND': [ 17 | { 18 | '=': ['name', 'Shaun Persad'] 19 | }, 20 | { 21 | '>=': ['age', 27] 22 | } 23 | ] 24 | }); 25 | ``` 26 | 27 | You can also evaluate the query in-line as the expressions are being built. 28 | 29 | ```js 30 | const sql = 'name = "Shaun Persad" AND age >= (20 + 7)'; 31 | const parser = new SqlWhereParser(); 32 | /** 33 | * This evaluator function will evaluate the "+" operator with its operands by adding its operands together. 34 | */ 35 | const parsed = parser.parse(sql, (operatorValue, operands) => { 36 | 37 | if (operatorValue === '+') { 38 | return operands[0] + operands[1]; 39 | } 40 | return parser.defaultEvaluator(operatorValue, operands); 41 | }); 42 | 43 | equals(parsed, { 44 | 'AND': [ 45 | { 46 | '=': ['name', 'Shaun Persad'] 47 | }, 48 | { 49 | '>=': ['age', 27] 50 | } 51 | ] 52 | }); 53 | ``` 54 | 55 | This evaluation can also be used to convert the AST into a specific tree, like a MongoDB query. 56 | 57 | ```js 58 | const sql = 'name = "Shaun Persad" AND age >= 27'; 59 | const parser = new SqlWhereParser(); 60 | /** 61 | * This will map each operand to its mongoDB equivalent. 62 | */ 63 | const parsed = parser.parse(sql, (operatorValue, operands) => { 64 | switch (operatorValue) { 65 | case '=': 66 | return { 67 | [operands[0]]: operands[1] 68 | }; 69 | case 'AND': 70 | return { 71 | $and: operands 72 | }; 73 | case '>=': 74 | return { 75 | [operands[0]]: { 76 | $gte: operands[1] 77 | } 78 | }; 79 | } 80 | }); 81 | 82 | equals(parsed, { 83 | $and: [ 84 | { 85 | name: 'Shaun Persad' 86 | }, 87 | { 88 | age: { 89 | $gte: 27 90 | } 91 | } 92 | ] 93 | }); 94 | ``` 95 | 96 | SqlWhereParser can also parse into an array-like structure, where each sub-array is its own group of parentheses in the SQL. 97 | 98 | ```js 99 | const sql = '(name = "Shaun Persad") AND (age >= (20 + 7))'; 100 | const parser = new SqlWhereParser(); 101 | const sqlArray = parser.toArray(sql); 102 | equals(sqlArray, [['name', '=', 'Shaun Persad'], 'AND', ['age', '>=', [20, '+', 7]]]); 103 | ``` 104 | 105 | This array structure is useful for displaying the query on the front-end, e.g. as HTML. 106 | 107 | ```js 108 | const sql = '(name = "Shaun Persad") AND age >= (20 + 7)'; 109 | const parser = new SqlWhereParser(); 110 | const sqlArray = parser.toArray(sql); 111 | /** 112 | * This function will recursively map the elements of the array to HTML. 113 | */ 114 | const toHtml = (toConvert) => { 115 | if (toConvert && toConvert.constructor === SqlWhereParser.Operator) { 116 | return `${toConvert}`; 117 | } 118 | 119 | if (!toConvert || !(toConvert.constructor === Array)) { 120 | 121 | return `${toConvert}`; 122 | } 123 | const html = toConvert.map((toConvert) => { 124 | return toHtml(toConvert); 125 | }); 126 | return `
${html.join('')}
`; 127 | }; 128 | 129 | const html = toHtml(sqlArray); 130 | equals(html, 131 | '
' + 132 | '
' + 133 | 'name' + 134 | '=' + 135 | 'Shaun Persad' + 136 | '
' + 137 | 'AND' + 138 | 'age' + 139 | '>=' + 140 | '
' + 141 | '20' + 142 | '+' + 143 | '7' + 144 | '
' + 145 | '
' 146 | ); 147 | ``` 148 | 149 | 150 | ## Installation 151 | 152 | `npm install sql-where-parser`. 153 | 154 | ```js 155 | // or if in the browser: 156 | ``` 157 | 158 | 159 | ## Usage 160 | 161 | `require` it, and create a new instance. 162 | 163 | ```js 164 | //const SqlWhereParser = require('sql-where-parser'); 165 | const sql = 'name = "Shaun Persad" AND age >= 27'; 166 | const parser = new SqlWhereParser(); 167 | 168 | const parsed = parser.parse(sql); // Abstract syntax tree 169 | const sqlArray = parser.toArray(sql); // Array 170 | ``` 171 | 172 | 173 | ## Advanced Usage 174 | ### Supplying a config object 175 | 176 | #### see [here](https://github.com/shaunpersad/sql-where-parser/blob/master/API.md#defaultconfigobject) for all options 177 | 178 | Modifying the config can be used to add new operators: 179 | 180 | ```js 181 | const config = SqlWhereParser.defaultConfig; // start off with the default config. 182 | config.operators[5]['<=>'] = 2; // number of operands to expect for this operator. 183 | config.operators[5]['<>'] = 2; // number of operands to expect for this operator. 184 | config.tokenizer.shouldTokenize.push('<=>', '<>'); 185 | const sql = 'name <> "Shaun Persad" AND age <=> 27'; 186 | const parser = new SqlWhereParser(config); // use the new config 187 | const parsed = parser.parse(sql); 188 | equals(parsed, { 189 | 'AND': [ 190 | { 191 | '<>': ['name', 'Shaun Persad'] 192 | }, 193 | { 194 | '<=>': ['age', 27] 195 | } 196 | ] 197 | }); 198 | ``` 199 | 200 | ## API 201 | For the full API documentation and more examples, see [here](https://github.com/shaunpersad/sql-where-parser/blob/master/API.md). -------------------------------------------------------------------------------- /SqlWhereParser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Symbol = require('es6-symbol'); 3 | const TokenizeThis = require('tokenize-this'); 4 | 5 | /** 6 | * To distinguish between the binary minus and unary. 7 | * 8 | * @type {Symbol} 9 | */ 10 | const OPERATOR_UNARY_MINUS = Symbol('-'); 11 | 12 | /** 13 | * Number of operands in a unary operation. 14 | * 15 | * @type {number} 16 | */ 17 | const OPERATOR_TYPE_UNARY = 1; 18 | 19 | /** 20 | * Number of operands in a binary operation. 21 | * 22 | * @type {number} 23 | */ 24 | const OPERATOR_TYPE_BINARY = 2; 25 | 26 | /** 27 | * Number of operands in a ternary operation. 28 | * 29 | * @type {number} 30 | */ 31 | const OPERATOR_TYPE_TERNARY = 3; 32 | 33 | /** 34 | * Defining the use of the unary minus. 35 | * 36 | * @type {{operators: [{}], tokenizer: {shouldTokenize: string[], shouldMatch: string[], shouldDelimitBy: string[]}}} 37 | */ 38 | const unaryMinusDefinition = { 39 | [OPERATOR_UNARY_MINUS]: OPERATOR_TYPE_UNARY 40 | }; 41 | 42 | /** 43 | * A wrapper class around operators to distinguish them from regular tokens. 44 | */ 45 | class Operator { 46 | 47 | constructor(value, type, precedence) { 48 | this.value = value; 49 | this.type = type; 50 | this.precedence = precedence; 51 | } 52 | toJSON() { 53 | return this.value; 54 | } 55 | toString() { 56 | return `${this.value}`; 57 | } 58 | } 59 | 60 | /** 61 | * The main parser class. 62 | */ 63 | class SqlWhereParser { 64 | 65 | /** 66 | * 67 | * @param {{operators: [{}], tokenizer: {shouldTokenize: string[], shouldMatch: string[], shouldDelimitBy: string[]}}} [config] 68 | */ 69 | constructor(config) { 70 | 71 | if (!config) { 72 | config = {}; 73 | } 74 | 75 | /** 76 | * 77 | * @type {{operators: [{}], tokenizer: {shouldTokenize: string[], shouldMatch: string[], shouldDelimitBy: string[]}}} 78 | */ 79 | config = Object.assign({}, this.constructor.defaultConfig, config); 80 | 81 | /** 82 | * 83 | * @type {TokenizeThis} 84 | */ 85 | this.tokenizer = new TokenizeThis(config.tokenizer); 86 | 87 | /** 88 | * 89 | * @type {{}} 90 | */ 91 | this.operators = {}; 92 | 93 | /** 94 | * Flattens the operator definitions into a single object, 95 | * whose keys are the operators, and the values are the Operator class wrappers. 96 | */ 97 | config.operators.forEach((operators, precedence) => { 98 | 99 | Object.keys(operators).concat(Object.getOwnPropertySymbols(operators)).forEach((operator) => { 100 | 101 | this.operators[operator] = new Operator(operator, operators[operator], precedence); 102 | }); 103 | }); 104 | } 105 | 106 | /** 107 | * 108 | * @param {string} sql 109 | * @param {function} [evaluator] 110 | * @returns {{}} 111 | */ 112 | parse(sql, evaluator) { 113 | 114 | const operatorStack = []; 115 | const outputStream = []; 116 | let lastOperator = undefined; 117 | let tokenCount = 0; 118 | let lastTokenWasOperatorOrLeftParenthesis = false; 119 | 120 | if (!evaluator) { 121 | evaluator = this.defaultEvaluator; 122 | } 123 | 124 | /** 125 | * The following mess is an implementation of the Shunting-Yard Algorithm: http://wcipeg.com/wiki/Shunting_yard_algorithm 126 | * See also: https://en.wikipedia.org/wiki/Shunting-yard_algorithm 127 | */ 128 | this.tokenizer.tokenize(`(${sql})`, (token, surroundedBy) => { 129 | 130 | tokenCount++; 131 | 132 | /** 133 | * Read a token. 134 | */ 135 | 136 | if (typeof token === 'string' && !surroundedBy) { 137 | 138 | let normalizedToken = token.toUpperCase(); 139 | 140 | /** 141 | * If the token is an operator, o1, then: 142 | */ 143 | if (this.operators[normalizedToken]) { 144 | 145 | /** 146 | * Hard-coded rule for between to ignore the next AND. 147 | */ 148 | if (lastOperator === 'BETWEEN' && normalizedToken === 'AND') { 149 | lastOperator = 'AND'; 150 | return; 151 | } 152 | 153 | /** 154 | * If the conditions are right for unary minus, convert it. 155 | */ 156 | if (normalizedToken === '-' && (tokenCount === 1 || lastTokenWasOperatorOrLeftParenthesis)) { 157 | normalizedToken = OPERATOR_UNARY_MINUS; 158 | } 159 | 160 | /** 161 | * While there is an operator token o2 at the top of the operator stack, 162 | * and o1's precedence is less than or equal to that of o2, 163 | * pop o2 off the operator stack, onto the output queue: 164 | */ 165 | while (operatorStack[operatorStack.length - 1] && operatorStack[operatorStack.length - 1] !== '(' && this.operatorPrecedenceFromValues(normalizedToken, operatorStack[operatorStack.length - 1])) { 166 | 167 | const operator = this.operators[operatorStack.pop()]; 168 | const operands = []; 169 | let numOperands = operator.type; 170 | while (numOperands--) { 171 | operands.unshift(outputStream.pop()); 172 | } 173 | outputStream.push(evaluator(operator.value, operands)); 174 | } 175 | 176 | /** 177 | * At the end of iteration push o1 onto the operator stack. 178 | */ 179 | operatorStack.push(normalizedToken); 180 | lastOperator = normalizedToken; 181 | 182 | lastTokenWasOperatorOrLeftParenthesis = true; 183 | 184 | /** 185 | * If the token is a left parenthesis (i.e. "("), then push it onto the stack: 186 | */ 187 | } else if (token === '(') { 188 | 189 | operatorStack.push(token); 190 | lastTokenWasOperatorOrLeftParenthesis = true; 191 | 192 | /** 193 | * If the token is a right parenthesis (i.e. ")"): 194 | */ 195 | } else if (token === ')') { 196 | 197 | /** 198 | * Until the token at the top of the stack is a left parenthesis, 199 | * pop operators off the stack onto the output queue. 200 | */ 201 | while(operatorStack.length && operatorStack[operatorStack.length - 1] !== '(') { 202 | 203 | const operator = this.operators[operatorStack.pop()]; 204 | const operands = []; 205 | let numOperands = operator.type; 206 | while (numOperands--) { 207 | operands.unshift(outputStream.pop()); 208 | } 209 | 210 | outputStream.push(evaluator(operator.value, operands)); 211 | } 212 | if (!operatorStack.length) { 213 | throw new SyntaxError('Unmatched parenthesis.'); 214 | } 215 | /** 216 | * Pop the left parenthesis from the stack, but not onto the output queue. 217 | */ 218 | operatorStack.pop(); 219 | lastTokenWasOperatorOrLeftParenthesis = false; 220 | 221 | /** 222 | * Push everything else to the output queue. 223 | */ 224 | } else { 225 | outputStream.push(token); 226 | lastTokenWasOperatorOrLeftParenthesis = false; 227 | } 228 | 229 | /** 230 | * Push explicit strings to the output queue. 231 | */ 232 | } else { 233 | outputStream.push(token); 234 | lastTokenWasOperatorOrLeftParenthesis = false; 235 | } 236 | }); 237 | 238 | 239 | /** 240 | * While there are still operator tokens in the stack: 241 | */ 242 | while (operatorStack.length) { 243 | 244 | const operatorValue = operatorStack.pop(); 245 | 246 | /** 247 | * If the operator token on the top of the stack is a parenthesis, then there are mismatched parentheses. 248 | */ 249 | if (operatorValue === '(') { 250 | throw new SyntaxError('Unmatched parenthesis.'); 251 | } 252 | const operator = this.operators[operatorValue]; 253 | const operands = []; 254 | let numOperands = operator.type; 255 | while (numOperands--) { 256 | operands.unshift(outputStream.pop()); 257 | } 258 | 259 | /** 260 | * Pop the operator onto the output queue. 261 | */ 262 | outputStream.push(evaluator(operator.value, operands)); 263 | } 264 | 265 | if (outputStream.length > 1) { 266 | throw new SyntaxError('Could not reduce to a single expression.'); 267 | } 268 | 269 | return outputStream[0]; 270 | } 271 | 272 | /** 273 | * 274 | * @param {string} sql 275 | * @returns {[]} 276 | */ 277 | toArray(sql) { 278 | 279 | let expression = []; 280 | let tokenCount = 0; 281 | let lastToken = undefined; 282 | const expressionParentheses = []; 283 | 284 | this.tokenizer.tokenize(`(${sql})`, (token, surroundedBy) => { 285 | 286 | tokenCount++; 287 | 288 | switch (token) { 289 | case '(': 290 | expressionParentheses.push(expression.length); 291 | break; 292 | case ')': 293 | const precedenceParenthesisIndex = expressionParentheses.pop(); 294 | 295 | let expressionTokens = expression.splice(precedenceParenthesisIndex, expression.length); 296 | 297 | while(expressionTokens && expressionTokens.constructor === Array && expressionTokens.length === 1) { 298 | expressionTokens = expressionTokens[0]; 299 | } 300 | expression.push(expressionTokens); 301 | break; 302 | case '': 303 | break; 304 | case ',': 305 | break; 306 | default: 307 | let operator = null; 308 | if (!surroundedBy) { 309 | operator = this.getOperator(token); 310 | if (token === '-' && (tokenCount === 1 || (lastToken === '(' || (lastToken && lastToken.constructor === Operator)))) { 311 | operator = this.getOperator(OPERATOR_UNARY_MINUS); 312 | } 313 | } 314 | expression.push(operator ? operator : token); 315 | break; 316 | } 317 | lastToken = token; 318 | }); 319 | 320 | while(expression && expression.constructor === Array && expression.length === 1) { 321 | expression = expression[0]; 322 | } 323 | 324 | return expression; 325 | } 326 | 327 | /** 328 | * 329 | * @param {string|Symbol} operatorValue1 330 | * @param {string|Symbol} operatorValue2 331 | * @returns {boolean} 332 | */ 333 | operatorPrecedenceFromValues(operatorValue1, operatorValue2) { 334 | 335 | return this.operators[operatorValue2].precedence <= this.operators[operatorValue1].precedence; 336 | } 337 | 338 | /** 339 | * 340 | * @param {string|Symbol} operatorValue 341 | * @returns {*} 342 | */ 343 | getOperator(operatorValue) { 344 | 345 | if (typeof operatorValue === 'string') { 346 | return this.operators[operatorValue.toUpperCase()]; 347 | } 348 | if (typeof operatorValue === 'symbol') { 349 | return this.operators[operatorValue]; 350 | } 351 | return null; 352 | } 353 | 354 | 355 | /** 356 | * 357 | * @param {string|Symbol} operatorValue 358 | * @param {[]} operands 359 | * @returns {*} 360 | */ 361 | defaultEvaluator(operatorValue, operands) { 362 | 363 | /** 364 | * Convert back to regular minus, now that we have the proper number of operands. 365 | */ 366 | if (operatorValue === OPERATOR_UNARY_MINUS) { 367 | operatorValue = '-'; 368 | } 369 | /** 370 | * This is a trick to avoid the problem of inconsistent comma usage in SQL. 371 | */ 372 | if (operatorValue === ',') { 373 | return [].concat(operands[0], operands[1]); 374 | } 375 | 376 | return { 377 | [operatorValue]: operands 378 | }; 379 | } 380 | 381 | /** 382 | * 383 | * @returns {{operators: [{}], tokenizer: {shouldTokenize: string[], shouldMatch: string[], shouldDelimitBy: string[]}}} 384 | */ 385 | static get defaultConfig() { 386 | 387 | return { 388 | operators: [ // TODO: add more operator definitions 389 | { 390 | '!': OPERATOR_TYPE_UNARY 391 | }, 392 | unaryMinusDefinition, 393 | { 394 | '^': OPERATOR_TYPE_BINARY 395 | }, 396 | { 397 | '*': OPERATOR_TYPE_BINARY, 398 | '/': OPERATOR_TYPE_BINARY, 399 | '%': OPERATOR_TYPE_BINARY 400 | }, 401 | { 402 | '+': OPERATOR_TYPE_BINARY, 403 | '-': OPERATOR_TYPE_BINARY 404 | }, 405 | { 406 | '=': OPERATOR_TYPE_BINARY, 407 | '<': OPERATOR_TYPE_BINARY, 408 | '>': OPERATOR_TYPE_BINARY, 409 | '<=': OPERATOR_TYPE_BINARY, 410 | '>=': OPERATOR_TYPE_BINARY, 411 | '!=': OPERATOR_TYPE_BINARY 412 | }, 413 | { 414 | ',': OPERATOR_TYPE_BINARY // We treat commas as an operator, to aid in turning arbitrary numbers of comma-separated values into arrays. 415 | }, 416 | { 417 | 'NOT': OPERATOR_TYPE_UNARY 418 | }, 419 | { 420 | 'BETWEEN': OPERATOR_TYPE_TERNARY, 421 | 'IN': OPERATOR_TYPE_BINARY, 422 | 'IS': OPERATOR_TYPE_BINARY, 423 | 'LIKE': OPERATOR_TYPE_BINARY 424 | }, 425 | { 426 | 'AND': OPERATOR_TYPE_BINARY 427 | }, 428 | { 429 | 'OR': OPERATOR_TYPE_BINARY 430 | } 431 | ], 432 | tokenizer: { 433 | shouldTokenize: ['(', ')', ',', '*', '/', '%', '+', '-', '=', '!=','!', '<', '>', '<=', '>=', '^'], 434 | shouldMatch: ['"', "'", '`'], 435 | shouldDelimitBy: [' ', "\n", "\r", "\t"] 436 | } 437 | }; 438 | } 439 | 440 | static get Operator() { 441 | return Operator; 442 | } 443 | 444 | static get OPERATOR_UNARY_MINUS() { 445 | return OPERATOR_UNARY_MINUS; 446 | } 447 | } 448 | 449 | /** 450 | * 451 | * @type {SqlWhereParser} 452 | */ 453 | module.exports = SqlWhereParser; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var browserify = require('browserify'); 3 | var babelify = require('babelify'); 4 | var source = require('vinyl-source-stream'); 5 | var buffer = require('vinyl-buffer'); 6 | var uglify = require('gulp-uglify'); 7 | 8 | gulp.task('browserify', function() { 9 | 10 | browserify({ 11 | entries: './SqlWhereParser.js', 12 | debug: true, 13 | standalone: 'SqlWhereParser' 14 | }) 15 | .transform(babelify, {presets: ['es2015']}) 16 | .bundle() 17 | .pipe(source('sql-where-parser.min.js')) 18 | .pipe(buffer()) 19 | .pipe(uglify()) 20 | .pipe(gulp.dest('./')); 21 | 22 | }); 23 | 24 | gulp.task('watch', function() { 25 | gulp.watch(['SqlWhereParser.js', 'TokenizeThis.js'], ['browserify']); 26 | }); 27 | 28 | gulp.task('default', ['browserify', 'watch']); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-where-parser", 3 | "version": "2.2.1", 4 | "description": "Parses an SQL-like WHERE string into various forms.", 5 | "main": "SqlWhereParser.js", 6 | "scripts": { 7 | "test": "./node_modules/mocha/bin/mocha --recursive", 8 | "document": "./node_modules/mocha/bin/mocha --recursive --reporter=markdown" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/shaunpersad/sql-where-parser.git" 13 | }, 14 | "keywords": [ 15 | "SQL", 16 | "parser", 17 | "WHERE", 18 | "AST", 19 | "abstract", 20 | "syntax", 21 | "tree", 22 | "convert", 23 | "mongo", 24 | "elasticsearch" 25 | ], 26 | "author": "Shaun Persad (http://github.com/shaunpersad)", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/shaunpersad/sql-where-parser/issues" 30 | }, 31 | "homepage": "https://github.com/shaunpersad/sql-where-parser#readme", 32 | "devDependencies": { 33 | "babel-preset-es2015": "^6.18.0", 34 | "babelify": "^7.3.0", 35 | "browserify": "^13.1.1", 36 | "gulp": "^3.9.1", 37 | "gulp-concat": "^2.6.1", 38 | "gulp-uglify": "^2.0.0", 39 | "mocha": "^3.2.0", 40 | "should": "^11.1.2", 41 | "vinyl-buffer": "^1.0.0", 42 | "vinyl-source-stream": "^1.1.0" 43 | }, 44 | "dependencies": { 45 | "es6-symbol": "^3.1.0", 46 | "tokenize-this": "^1.4.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sql-where-parser.min.js: -------------------------------------------------------------------------------- 1 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.SqlWhereParser=t()}}(function(){var t;return function t(e,r,n){function o(s,u){if(!r[s]){if(!e[s]){var c="function"==typeof require&&require;if(!u&&c)return c(s,!0);if(i)return i(s,!0);var a=new Error("Cannot find module '"+s+"'");throw a.code="MODULE_NOT_FOUND",a}var f=r[s]={exports:{}};e[s][0].call(f.exports,function(t){var r=e[s][1][t];return o(r?r:t)},f,f.exports,t,e,r,n)}return r[s].exports}for(var i="function"==typeof require&&require,s=0;s1)throw new SyntaxError("Could not reduce to a single expression.");return o[0]}},{key:"toArray",value:function(t){var e=this,r=[],n=0,o=void 0,i=[];for(this.tokenizer.tokenize("("+t+")",function(t,s){switch(n++,t){case"(":i.push(r.length);break;case")":for(var u=i.pop(),c=r.splice(u,r.length);c&&c.constructor===Array&&1===c.length;)c=c[0];r.push(c);break;case"":break;case",":break;default:var f=null;s||(f=e.getOperator(t),"-"===t&&(1===n||"("===o||o&&o.constructor===y)&&(f=e.getOperator(a))),r.push(f?f:t)}o=t});r&&r.constructor===Array&&1===r.length;)r=r[0];return r}},{key:"operatorPrecedenceFromValues",value:function(t,e){return this.operators[e].precedence<=this.operators[t].precedence}},{key:"getOperator",value:function(t){return"string"==typeof t?this.operators[t.toUpperCase()]:"symbol"===("undefined"==typeof t?"undefined":i(t))?this.operators[t]:null}},{key:"defaultEvaluator",value:function(t,e){return t===a&&(t="-"),","===t?[].concat(e[0],e[1]):o({},t,e)}}],[{key:"defaultConfig",get:function(){return{operators:[{"!":f},h,{"^":l},{"*":l,"/":l,"%":l},{"+":l,"-":l},{"=":l,"<":l,">":l,"<=":l,">=":l,"!=":l},{",":l},{NOT:f},{BETWEEN:p,IN:l,IS:l,LIKE:l},{AND:l},{OR:l}],tokenizer:{shouldTokenize:["(",")",",","*","/","%","+","-","=","!=","!","<",">","<=",">=","^"],shouldMatch:['"',"'","`"],shouldDelimitBy:[" ","\n","\r","\t"]}}}},{key:"Operator",get:function(){return y}},{key:"OPERATOR_UNARY_MINUS",get:function(){return a}}]),t}();e.exports=d},{"es6-symbol":2,"tokenize-this":20}],2:[function(t,e,r){"use strict";e.exports=t("./is-implemented")()?Symbol:t("./polyfill")},{"./is-implemented":3,"./polyfill":18}],3:[function(t,e,r){"use strict";var n={object:!0,symbol:!0};e.exports=function(){var t;if("function"!=typeof Symbol)return!1;t=Symbol("test symbol");try{String(t)}catch(t){return!1}return!!n[typeof Symbol.iterator]&&(!!n[typeof Symbol.toPrimitive]&&!!n[typeof Symbol.toStringTag])}},{}],4:[function(t,e,r){"use strict";e.exports=function(t){return!!t&&("symbol"==typeof t||!!t.constructor&&("Symbol"===t.constructor.name&&"Symbol"===t[t.constructor.toStringTag]))}},{}],5:[function(t,e,r){"use strict";var n,o=t("es5-ext/object/assign"),i=t("es5-ext/object/normalize-options"),s=t("es5-ext/object/is-callable"),u=t("es5-ext/string/#/contains");n=e.exports=function(t,e){var r,n,s,c,a;return arguments.length<2||"string"!=typeof t?(c=e,e=t,t=null):c=arguments[2],null==t?(r=s=!0,n=!1):(r=u.call(t,"c"),n=u.call(t,"e"),s=u.call(t,"w")),a={value:e,configurable:r,enumerable:n,writable:s},c?o(i(c),a):a},n.gs=function(t,e,r){var n,c,a,f;return"string"!=typeof t?(a=r,r=e,e=t,t=null):a=arguments[3],null==e?e=void 0:s(e)?null==r?r=void 0:s(r)||(a=r,r=void 0):(a=e,e=r=void 0),null==t?(n=!0,c=!1):(n=u.call(t,"c"),c=u.call(t,"e")),f={get:e,set:r,configurable:n,enumerable:c},a?o(i(a),f):f}},{"es5-ext/object/assign":6,"es5-ext/object/is-callable":9,"es5-ext/object/normalize-options":13,"es5-ext/string/#/contains":15}],6:[function(t,e,r){"use strict";e.exports=t("./is-implemented")()?Object.assign:t("./shim")},{"./is-implemented":7,"./shim":8}],7:[function(t,e,r){"use strict";e.exports=function(){var t,e=Object.assign;return"function"==typeof e&&(t={foo:"raz"},e(t,{bar:"dwa"},{trzy:"trzy"}),t.foo+t.bar+t.trzy==="razdwatrzy")}},{}],8:[function(t,e,r){"use strict";var n=t("../keys"),o=t("../valid-value"),i=Math.max;e.exports=function(t,e){var r,s,u,c=i(arguments.length,2);for(t=Object(o(t)),u=function(n){try{t[n]=e[n]}catch(t){r||(r=t)}},s=1;s-1}},{}],18:[function(t,e,r){"use strict";var n,o,i,s,u=t("d"),c=t("./validate-symbol"),a=Object.create,f=Object.defineProperties,l=Object.defineProperty,p=Object.prototype,h=a(null);if("function"==typeof Symbol){n=Symbol;try{String(n()),s=!0}catch(t){}}var y=function(){var t=a(null);return function(e){for(var r,n,o=0;t[e+(o||"")];)++o;return e+=o||"",t[e]=!0,r="@@"+e,l(p,r,u.gs(null,function(t){n||(n=!0,l(this,r,u(t)),n=!1)})),r}}();i=function(t){if(this instanceof i)throw new TypeError("TypeError: Symbol is not a constructor");return o(t)},e.exports=o=function t(e){var r;if(this instanceof t)throw new TypeError("TypeError: Symbol is not a constructor");return s?n(e):(r=a(i.prototype),e=void 0===e?"":String(e),f(r,{__description__:u("",e),__name__:u("",y(e))}))},f(o,{for:u(function(t){return h[t]?h[t]:h[t]=o(String(t))}),keyFor:u(function(t){var e;c(t);for(e in h)if(h[e]===t)return e}),hasInstance:u("",n&&n.hasInstance||o("hasInstance")),isConcatSpreadable:u("",n&&n.isConcatSpreadable||o("isConcatSpreadable")),iterator:u("",n&&n.iterator||o("iterator")),match:u("",n&&n.match||o("match")),replace:u("",n&&n.replace||o("replace")),search:u("",n&&n.search||o("search")),species:u("",n&&n.species||o("species")),split:u("",n&&n.split||o("split")),toPrimitive:u("",n&&n.toPrimitive||o("toPrimitive")),toStringTag:u("",n&&n.toStringTag||o("toStringTag")),unscopables:u("",n&&n.unscopables||o("unscopables"))}),f(i.prototype,{constructor:u(o),toString:u("",function(){return this.__name__})}),f(o.prototype,{toString:u(function(){return"Symbol ("+c(this).__description__+")"}),valueOf:u(function(){return c(this)})}),l(o.prototype,o.toPrimitive,u("",function(){var t=c(this);return"symbol"==typeof t?t:t.toString()})),l(o.prototype,o.toStringTag,u("c","Symbol")),l(i.prototype,o.toStringTag,u("c",o.prototype[o.toStringTag])),l(i.prototype,o.toPrimitive,u("c",o.prototype[o.toPrimitive]))},{"./validate-symbol":19,d:5}],19:[function(t,e,r){"use strict";var n=t("./is-symbol");e.exports=function(t){if(!n(t))throw new TypeError(t+" is not a symbol");return t}},{"./is-symbol":4}],20:[function(e,r,n){(function(o){!function(e){if("object"==typeof n&&"undefined"!=typeof r)r.exports=e();else if("function"==typeof t&&t.amd)t([],e);else{var i;i="undefined"!=typeof window?window:"undefined"!=typeof o?o:"undefined"!=typeof self?self:this,i.TokenizeThis=e()}}(function(){return function t(r,n,o){function i(u,c){if(!n[u]){if(!r[u]){var a="function"==typeof e&&e;if(!c&&a)return a(u,!0);if(s)return s(u,!0);var f=new Error("Cannot find module '"+u+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[u]={exports:{}};r[u][0].call(l.exports,function(t){var e=r[u][1][t];return i(e?e:t)},l,l.exports,t,r,n,o)}return n[u].exports}for(var s="function"==typeof e&&e,u=0;ue.length?-1:t.length0&&this.push(this.currentToken.substring(0,e)),e!==-1?(this.push(r),this.currentToken=this.currentToken.substring(e+r.length),this.pushDefaultModeTokenizables()):void 0}},{key:u,value:function(t){if(t===this.toMatch){if(this.previousChr!==this.factory.escapeCharacter)return this.completeCurrentMode();this.currentToken=this.currentToken.substring(0,this.currentToken.length-1)}return this.currentToken+=t,this.currentToken}}]),t}(),f=function(){function t(e){var r=this;n(this,t),e||(e={}),e=Object.assign({},this.constructor.defaultConfig,e),this.convertLiterals=e.convertLiterals,this.escapeCharacter=e.escapeCharacter,this.tokenizeList=[],this.tokenizeMap={},this.matchList=[],this.matchMap={},this.delimiterList=[],this.delimiterMap={},e.shouldTokenize.sort(c).forEach(function(t){r.tokenizeMap[t]||(r.tokenizeList.push(t),r.tokenizeMap[t]=t)}),e.shouldMatch.forEach(function(t){r.matchMap[t]||(r.matchList.push(t),r.matchMap[t]=t)}),e.shouldDelimitBy.forEach(function(t){r.delimiterMap[t]||(r.delimiterList.push(t),r.delimiterMap[t]=t)})}return o(t,[{key:"tokenize",value:function(t,e){var r=new a(this,t,e);return r.tokenize()}}],[{key:"defaultConfig",get:function(){return{shouldTokenize:["(",")",",","*","/","%","+","-","=","!=","!","<",">","<=",">=","^"],shouldMatch:['"',"'","`"],shouldDelimitBy:[" ","\n","\r","\t"],convertLiterals:!0,escapeCharacter:"\\"}}}]),t}();e.exports=f},{}]},{},[1])(1)})}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[1])(1)}); -------------------------------------------------------------------------------- /test/SqlWhereParser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const should = require('should'); 3 | const SqlWhereParser = require('../SqlWhereParser'); 4 | const TokenizeThis = require('tokenize-this'); 5 | 6 | function equals(obj1, obj2) { 7 | 8 | if (JSON.stringify(obj1) !== JSON.stringify(obj2)) { 9 | throw new Error('The two objects are not the same.'); 10 | } 11 | } 12 | 13 | describe('SqlWhereParser', function() { 14 | 15 | describe('What is it?', function() { 16 | 17 | it('SqlWhereParser parses the WHERE portion of an SQL-like string into an abstract syntax tree', function() { 18 | 19 | const sql = 'name = "Shaun Persad" AND age >= 27'; 20 | const parser = new SqlWhereParser(); 21 | const parsed = parser.parse(sql); 22 | 23 | /** 24 | * The tree is object-based, where each key is the operator, and its value is an array of the operands. 25 | * The number of operands depends on if the operation is defined as unary, binary, or ternary in the config. 26 | */ 27 | equals(parsed, { 28 | 'AND': [ 29 | { 30 | '=': ['name', 'Shaun Persad'] 31 | }, 32 | { 33 | '>=': ['age', 27] 34 | } 35 | ] 36 | }); 37 | }); 38 | 39 | it('You can also evaluate the query in-line as the expressions are being built', function() { 40 | 41 | const sql = 'name = "Shaun Persad" AND age >= (20 + 7)'; 42 | const parser = new SqlWhereParser(); 43 | 44 | /** 45 | * This evaluator function will evaluate the "+" operator with its operands by adding its operands together. 46 | */ 47 | const parsed = parser.parse(sql, (operatorValue, operands) => { 48 | 49 | if (operatorValue === '+') { 50 | return operands[0] + operands[1]; 51 | } 52 | return parser.defaultEvaluator(operatorValue, operands); 53 | }); 54 | 55 | equals(parsed, { 56 | 'AND': [ 57 | { 58 | '=': ['name', 'Shaun Persad'] 59 | }, 60 | { 61 | '>=': ['age', 27] 62 | } 63 | ] 64 | }); 65 | }); 66 | 67 | it('This evaluation can also be used to convert the AST into a specific tree, like a MongoDB query', function() { 68 | 69 | const sql = 'name = "Shaun Persad" AND age >= 27'; 70 | const parser = new SqlWhereParser(); 71 | 72 | /** 73 | * This will map each operand to its mongoDB equivalent. 74 | */ 75 | const parsed = parser.parse(sql, (operatorValue, operands) => { 76 | 77 | switch (operatorValue) { 78 | case '=': 79 | return { 80 | [operands[0]]: operands[1] 81 | }; 82 | case 'AND': 83 | return { 84 | $and: operands 85 | }; 86 | case '>=': 87 | return { 88 | [operands[0]]: { 89 | $gte: operands[1] 90 | } 91 | }; 92 | } 93 | }); 94 | 95 | equals(parsed, { 96 | $and: [ 97 | { 98 | name: 'Shaun Persad' 99 | }, 100 | { 101 | age: { 102 | $gte: 27 103 | } 104 | } 105 | ] 106 | }); 107 | }); 108 | 109 | it('SqlWhereParser can also parse into an array-like structure, where each sub-array is its own group of parentheses in the SQL', function() { 110 | 111 | const sql = '(name = "Shaun Persad") AND (age >= (20 + 7))'; 112 | const parser = new SqlWhereParser(); 113 | 114 | const sqlArray = parser.toArray(sql); 115 | 116 | equals(sqlArray, [['name', '=', 'Shaun Persad'], 'AND', ['age', '>=', [20, '+', 7]]]); 117 | }); 118 | 119 | it('This array structure is useful for displaying the query on the front-end, e.g. as HTML', function() { 120 | 121 | const sql = '(name = "Shaun Persad") AND age >= (20 + 7)'; 122 | const parser = new SqlWhereParser(); 123 | 124 | const sqlArray = parser.toArray(sql); 125 | 126 | /** 127 | * This function will recursively map the elements of the array to HTML. 128 | */ 129 | const toHtml = (toConvert) => { 130 | 131 | if (toConvert && toConvert.constructor === SqlWhereParser.Operator) { 132 | 133 | return `${toConvert}`; 134 | } 135 | 136 | if (!toConvert || !(toConvert.constructor === Array)) { 137 | 138 | return `${toConvert}`; 139 | } 140 | 141 | const html = toConvert.map((toConvert) => { 142 | 143 | return toHtml(toConvert); 144 | }); 145 | 146 | return `
${html.join('')}
`; 147 | }; 148 | 149 | const html = toHtml(sqlArray); 150 | 151 | equals(html, 152 | '
' + 153 | '
' + 154 | 'name' + 155 | '=' + 156 | 'Shaun Persad' + 157 | '
' + 158 | 'AND' + 159 | 'age' + 160 | '>=' + 161 | '
' + 162 | '20' + 163 | '+' + 164 | '7' + 165 | '
' + 166 | '
' 167 | ); 168 | }); 169 | }); 170 | 171 | describe('Installation', function() { 172 | 173 | it('`npm install sql-where-parser`', function() { 174 | // or if in the browser: 175 | }); 176 | }); 177 | 178 | describe('Usage', function() { 179 | 180 | it('`require` it, and create a new instance', function() { 181 | 182 | //const SqlWhereParser = require('sql-where-parser'); 183 | const sql = 'name = "Shaun Persad" AND age >= 27'; 184 | const parser = new SqlWhereParser(); 185 | 186 | const parsed = parser.parse(sql); 187 | const sqlArray = parser.toArray(sql); 188 | }); 189 | }); 190 | 191 | describe('Advanced Usage', function() { 192 | 193 | describe('Supplying a config object', function() { 194 | 195 | describe('see [here](#defaultconfigobject) for all options', function() { 196 | 197 | it('This can be used to create new operators', function() { 198 | 199 | const config = SqlWhereParser.defaultConfig; // start off with the default config. 200 | 201 | config.operators[5]['<=>'] = 2; // number of operands to expect for this operator. 202 | config.operators[5]['<>'] = 2; // number of operands to expect for this operator. 203 | 204 | config.tokenizer.shouldTokenize.push('<=>', '<>'); 205 | 206 | const sql = 'name <> "Shaun Persad" AND age <=> 27'; 207 | const parser = new SqlWhereParser(config); // use the new config 208 | 209 | const parsed = parser.parse(sql); 210 | 211 | equals(parsed, { 212 | 'AND': [ 213 | { 214 | '<>': ['name', 'Shaun Persad'] 215 | }, 216 | { 217 | '<=>': ['age', 27] 218 | } 219 | ] 220 | }); 221 | }); 222 | }); 223 | }); 224 | }); 225 | 226 | describe('API', function() { 227 | 228 | describe('Methods', function() { 229 | 230 | describe('#parse(sql:String):Object', function() { 231 | 232 | it('Parses the SQL string into an AST with the proper order of operations', function() { 233 | 234 | const sql = 'name = "Shaun Persad" AND job = developer AND (gender = male OR type = person AND location IN (NY, America) AND hobby = coding)'; 235 | const parser = new SqlWhereParser(); 236 | const parsed = parser.parse(sql); 237 | 238 | /** 239 | * Original. 240 | */ 241 | //'name = "Shaun Persad" AND job = developer AND (gender = male OR type = person AND location IN (NY, America) AND hobby = coding)'; 242 | /** 243 | * Perform equals. 244 | */ 245 | //'(name = "Shaun Persad") AND (job = developer) AND ((gender = male) OR (type = person) AND location IN (NY, America) AND (hobby = coding))'; 246 | /** 247 | * Perform IN 248 | */ 249 | //'(name = "Shaun Persad") AND (job = developer) AND ((gender = male) OR (type = person) AND (location IN (NY, America)) AND (hobby = coding))'; 250 | /** 251 | * Perform AND 252 | */ 253 | //'(((name = "Shaun Persad") AND (job = developer)) AND ((gender = male) OR (((type = person) AND (location IN (NY, America))) AND (hobby = coding))))'; 254 | 255 | equals(parsed, { 256 | 'AND': [ 257 | { 258 | 'AND': [ 259 | { 260 | '=': ['name', 'Shaun Persad'] 261 | }, 262 | { 263 | '=': ['job', 'developer'] 264 | } 265 | ] 266 | }, 267 | { 268 | 'OR': [ 269 | { 270 | '=': ['gender', 'male'] 271 | }, 272 | { 273 | 'AND': [ 274 | { 275 | 'AND': [ 276 | { 277 | '=': ['type', 'person'] 278 | }, 279 | { 280 | 'IN': ['location', ['NY', 'America']] 281 | } 282 | ] 283 | }, 284 | { 285 | '=': ['hobby', 'coding'] 286 | } 287 | ] 288 | } 289 | ] 290 | } 291 | ] 292 | }); 293 | }); 294 | 295 | it('It handles the unary minus case appropriately', function() { 296 | 297 | const parser = new SqlWhereParser(); 298 | let parsed = parser.parse('1 + -5'); 299 | 300 | equals(parsed, { 301 | '+': [ 302 | 1, 303 | { 304 | '-': [5] 305 | } 306 | ] 307 | }); 308 | 309 | parsed = parser.parse('-1 + -(5 - - 5)'); 310 | 311 | equals(parsed, { 312 | '+': [ 313 | { 314 | '-': [1] 315 | }, 316 | { 317 | '-': [ 318 | { 319 | '-': [ 320 | 5, 321 | { 322 | '-': [5] 323 | } 324 | ] 325 | } 326 | ] 327 | } 328 | ] 329 | }); 330 | }); 331 | 332 | it('It handles the BETWEEN case appropriately', function() { 333 | 334 | const parser = new SqlWhereParser(); 335 | const parsed = parser.parse('A BETWEEN 5 AND 10 AND B = C'); 336 | 337 | equals(parsed, { 338 | 'AND': [ 339 | { 340 | 'BETWEEN': ['A', 5, 10] 341 | }, 342 | { 343 | '=': ['B', 'C'] 344 | } 345 | ] 346 | }); 347 | }); 348 | 349 | it('Unnecessarily nested parentheses do not matter', function() { 350 | 351 | const sql = '((((name = "Shaun Persad")) AND (age >= 27)))'; 352 | const parser = new SqlWhereParser(); 353 | const parsed = parser.parse(sql); 354 | 355 | equals(parsed, { 356 | 'AND': [ 357 | { 358 | '=': ['name', 'Shaun Persad' 359 | ] 360 | }, 361 | { 362 | '>=': ['age', 27] 363 | } 364 | ] 365 | }); 366 | }); 367 | 368 | it('Throws a SyntaxError if the supplied parentheses do not match', function() { 369 | 370 | const sql = '(name = "Shaun Persad" AND age >= 27'; 371 | const parser = new SqlWhereParser(); 372 | 373 | let failed = false; 374 | 375 | try { 376 | const parsed = parser.parse(sql); 377 | failed = false; 378 | 379 | } catch (e) { 380 | 381 | failed = (e.constructor === SyntaxError); 382 | } 383 | if (!failed) { 384 | throw new Error('A SyntaxError was not thrown.'); 385 | } 386 | }); 387 | }); 388 | 389 | describe('#parse(sql:String, evaluator:Function):*', function() { 390 | 391 | it('Uses the supplied `evaluator(operatorValue:String|Symbol, operands:Array)` function to convert an operator and its operands into its evaluation. ' + 392 | 'The default evaluator actually does no "evaluation" in the mathematical sense. ' + 393 | 'Instead it creates an object whose key is the operator, and the value is an array of the operands', function() { 394 | 395 | let timesCalled = 0; 396 | 397 | const sql = 'name = "Shaun Persad" AND age >= 27'; 398 | const parser = new SqlWhereParser(); 399 | 400 | const parsed = parser.parse(sql, (operatorValue, operands) => { 401 | 402 | timesCalled++; 403 | return parser.defaultEvaluator(operatorValue, operands); 404 | }); 405 | 406 | timesCalled.should.equal(3); 407 | 408 | equals(parsed, { 409 | 'AND': [ 410 | { 411 | '=': ['name', 'Shaun Persad' 412 | ] 413 | }, 414 | { 415 | '>=': ['age', 27] 416 | } 417 | ] 418 | }); 419 | }); 420 | 421 | it('"Evaluation" is subjective, and this can be exploited to convert the default object-based structure of the AST into something else, like this array-based structure', function() { 422 | 423 | const sql = 'name = "Shaun Persad" AND age >= 27'; 424 | const parser = new SqlWhereParser(); 425 | 426 | const parsed = parser.parse(sql, (operatorValue, operands) => { 427 | 428 | return [operatorValue, operands]; 429 | }); 430 | 431 | equals(parsed, [ 432 | 'AND', 433 | [ 434 | ['=',['name', 'Shaun Persad']], 435 | ['>=', ['age', 27]] 436 | ] 437 | ]); 438 | }); 439 | 440 | }); 441 | 442 | describe('#toArray(sql:String):Array', function() { 443 | 444 | it('Parses the SQL string into a nested array, where each expression is its own array', function() { 445 | 446 | const sql = '(name = "Shaun Persad") AND (age >= (20 + 7))'; 447 | const parser = new SqlWhereParser(); 448 | 449 | const sqlArray = parser.toArray(sql); 450 | 451 | equals(sqlArray, [ 452 | ['name', '=', 'Shaun Persad'], 'AND', ['age', '>=', [20, '+', 7]] 453 | ]); 454 | }); 455 | 456 | it('Unnecessarily nested parentheses do not matter', function() { 457 | 458 | const sql = '((((name = "Shaun Persad"))) AND ((age) >= ((20 + 7))))'; 459 | const parser = new SqlWhereParser(); 460 | 461 | const sqlArray = parser.toArray(sql); 462 | 463 | equals(sqlArray, [ 464 | ['name', '=', 'Shaun Persad'], 'AND', ['age', '>=', [20, '+', 7]] 465 | ]); 466 | 467 | }); 468 | }); 469 | 470 | describe('#operatorPrecedenceFromValues(operatorValue1:String|Symbol, operatorValue2:String|Symbol):Boolean', function() { 471 | 472 | it('Determines if operator 2 is of a higher precedence than operator 1', function() { 473 | 474 | const parser = new SqlWhereParser(); 475 | 476 | parser.operatorPrecedenceFromValues('AND', 'OR').should.equal(false); // AND is higher than OR 477 | 478 | parser.operatorPrecedenceFromValues('+', '-').should.equal(true); // + and - are equal 479 | 480 | parser.operatorPrecedenceFromValues('+', '*').should.equal(true); // * is higher than + 481 | 482 | /** 483 | * For full precedence list, check the [defaultConfig](#defaultconfigobject) object. 484 | */ 485 | }); 486 | 487 | it('It also works if either of the operator values are a Symbol instead of a String', function() { 488 | 489 | const parser = new SqlWhereParser(); 490 | 491 | parser.operatorPrecedenceFromValues(SqlWhereParser.OPERATOR_UNARY_MINUS, '-').should.equal(false); // unary minus is higher than minus 492 | }); 493 | }); 494 | 495 | describe('#getOperator(operatorValue:String|Symbol):Operator', function() { 496 | 497 | it('Returns the corresponding instance of the Operator class', function() { 498 | 499 | const parser = new SqlWhereParser(); 500 | const minus = parser.getOperator('-'); 501 | 502 | minus.should.be.instanceOf(SqlWhereParser.Operator); 503 | minus.should.have.property('value', '-'); 504 | minus.should.have.property('precedence', 4); 505 | minus.should.have.property('type', 2); // its binary 506 | }); 507 | 508 | it('It also works if the operator value is a Symbol instead of a String', function() { 509 | 510 | const parser = new SqlWhereParser(); 511 | const unaryMinus = parser.getOperator(SqlWhereParser.OPERATOR_UNARY_MINUS); 512 | 513 | unaryMinus.should.be.instanceOf(SqlWhereParser.Operator); 514 | unaryMinus.should.have.property('value', SqlWhereParser.OPERATOR_UNARY_MINUS); 515 | unaryMinus.should.have.property('precedence', 1); 516 | unaryMinus.should.have.property('type', 1); // its unary 517 | }); 518 | }); 519 | 520 | describe('#defaultEvaluator(operatorValue:String|Symbol, operands:Array)', function() { 521 | 522 | it('Converts the operator and its operands into an object whose key is the operator value, and the value is the array of operands', function() { 523 | 524 | const parser = new SqlWhereParser(); 525 | const evaluation = parser.defaultEvaluator('OPERATOR', [1, 2, 3]); 526 | 527 | equals(evaluation, { 528 | 'OPERATOR': [1, 2, 3] 529 | }); 530 | }); 531 | 532 | it('...Except for the special "," operator, which acts like a binary operator, but is not really an operator. It combines anything comma-separated into an array', function() { 533 | 534 | const parser = new SqlWhereParser(); 535 | const evaluation = parser.defaultEvaluator(',', [1, 2]); 536 | 537 | equals(evaluation, [1, 2]); 538 | }); 539 | 540 | it('When used in the recursive manner that it is, we are able to combine the results of several binary comma operations into a single array', function() { 541 | 542 | const parser = new SqlWhereParser(); 543 | const evaluation = parser.defaultEvaluator(',', [[1, 2], 3]); 544 | 545 | equals(evaluation, [1, 2, 3]); 546 | }); 547 | 548 | it('With the unary minus Symbol, it converts it back into a regular minus string, since the operands have been determined by this point', function() { 549 | 550 | const parser = new SqlWhereParser(); 551 | const evaluation = parser.defaultEvaluator(SqlWhereParser.OPERATOR_UNARY_MINUS, [1]); 552 | 553 | equals(evaluation, { 554 | '-': [1] 555 | }); 556 | }); 557 | }); 558 | 559 | describe('#tokenizer:TokenizeThis', function() { 560 | 561 | it('The tokenizer used on the string. See documentation [here](https://github.com/shaunpersad/tokenize-this)', function() { 562 | 563 | const parser = new SqlWhereParser(); 564 | parser.tokenizer.should.be.instanceOf(TokenizeThis); 565 | }); 566 | }); 567 | 568 | describe('#operators:Object', function() { 569 | 570 | it('An object whose keys are the supported operator values, and whose values are instances of the Operator class', function() { 571 | 572 | const parser = new SqlWhereParser(); 573 | const operators = ["!", SqlWhereParser.OPERATOR_UNARY_MINUS, "^","*","/","%","+","-","=","<",">","<=",">=","!=",",","NOT","BETWEEN","IN","IS","LIKE","AND","OR"]; 574 | 575 | operators.forEach((operator) => { 576 | 577 | parser.operators[operator].should.be.instanceOf(SqlWhereParser.Operator); 578 | }); 579 | }); 580 | }); 581 | 582 | describe('.defaultConfig:Object', function() { 583 | 584 | it('The default config object used when no config is supplied. For the tokenizer config options, see [here](https://github.com/shaunpersad/tokenize-this#defaultconfigobject)', function() { 585 | 586 | const OPERATOR_TYPE_UNARY = 1; 587 | const OPERATOR_TYPE_BINARY = 2; 588 | const OPERATOR_TYPE_TERNARY = 3; 589 | 590 | const unaryMinusDefinition = { 591 | [SqlWhereParser.OPERATOR_UNARY_MINUS]: OPERATOR_TYPE_UNARY 592 | }; 593 | 594 | equals(SqlWhereParser.defaultConfig, { 595 | operators: [ 596 | { 597 | '!': OPERATOR_TYPE_UNARY 598 | }, 599 | unaryMinusDefinition, 600 | { 601 | '^': OPERATOR_TYPE_BINARY 602 | }, 603 | { 604 | '*': OPERATOR_TYPE_BINARY, 605 | '/': OPERATOR_TYPE_BINARY, 606 | '%': OPERATOR_TYPE_BINARY 607 | }, 608 | { 609 | '+': OPERATOR_TYPE_BINARY, 610 | '-': OPERATOR_TYPE_BINARY 611 | }, 612 | { 613 | '=': OPERATOR_TYPE_BINARY, 614 | '<': OPERATOR_TYPE_BINARY, 615 | '>': OPERATOR_TYPE_BINARY, 616 | '<=': OPERATOR_TYPE_BINARY, 617 | '>=': OPERATOR_TYPE_BINARY, 618 | '!=': OPERATOR_TYPE_BINARY 619 | }, 620 | { 621 | ',': OPERATOR_TYPE_BINARY // We treat commas as an operator, to aid in turning arbitrary numbers of comma-separated values into arrays. 622 | }, 623 | { 624 | 'NOT': OPERATOR_TYPE_UNARY 625 | }, 626 | { 627 | 'BETWEEN': OPERATOR_TYPE_TERNARY, 628 | 'IN': OPERATOR_TYPE_BINARY, 629 | 'IS': OPERATOR_TYPE_BINARY, 630 | 'LIKE': OPERATOR_TYPE_BINARY 631 | }, 632 | { 633 | 'AND': OPERATOR_TYPE_BINARY 634 | }, 635 | { 636 | 'OR': OPERATOR_TYPE_BINARY 637 | } 638 | ], 639 | tokenizer: { 640 | shouldTokenize: ['(', ')', ',', '*', '/', '%', '+', '-', '=', '!=','!', '<', '>', '<=', '>=', '^'], 641 | shouldMatch: ['"', "'", '`'], 642 | shouldDelimitBy: [' ', "\n", "\r", "\t"] 643 | } 644 | }); 645 | }); 646 | }); 647 | 648 | describe('.Operator:Operator', function() { 649 | 650 | it('The Operator class', function() { 651 | 652 | const parser = new SqlWhereParser(); 653 | 654 | parser.operators['AND'].should.be.instanceOf(SqlWhereParser.Operator); 655 | }); 656 | }); 657 | 658 | describe('.OPERATOR_UNARY_MINUS:Symbol', function() { 659 | 660 | it('The Symbol used as the unary minus operator value', function() { 661 | 662 | (typeof SqlWhereParser.OPERATOR_UNARY_MINUS).should.equal('symbol'); 663 | }); 664 | }); 665 | 666 | }); 667 | }); 668 | }); 669 | --------------------------------------------------------------------------------