├── .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 |
--------------------------------------------------------------------------------