/**
* This class provides the ability to parse a `String` to produce the `ast` of an
* `Ext.data.Query` (that is, its Abstract Syntax Tree).
* @private
* @since 6.7.0
*/
Ext.define('Ext.data.query.Parser', function(QueryParser) { // eslint-disable-line brace-style
var LIST = { list: true, literal: true, type: 'list' };
return {
extend: 'Ext.parse.Parser',
tokenizer: {
keywords: {
and: {
type: 'operator',
name: 'and',
value: '&&',
is: { operator: true }
},
or: {
type: 'operator',
name: 'or',
value: '||',
is: { operator: true }
},
not: {
type: 'operator',
name: 'not',
value: '!',
is: { operator: true }
},
between: {
type: 'operator',
name: 'between',
value: 'between',
is: { operator: true }
},
like: {
type: 'operator',
name: 'like',
value: 'like',
is: { operator: true }
},
'in': {
type: 'operator',
name: 'in',
value: 'in',
is: { operator: true }
}
},
/* eslint-disable key-spacing */
operators: {
'=': 'eq',
'==': 'seq',
'===': 'seq',
'!==': 'sne',
'!=': 'neq',
'<>': 'neq',
'<': 'lt',
'<=': 'lte',
'>': 'gt',
'>=': 'gte',
'&&': 'and',
'||': 'or',
',': 'comma'
},
/* eslint-enable key-spacing */
patterns: {
regex: {
type: 'literal',
is: { literal: true, regexp: true, type: 'regexp' },
re: /\/(?!\/)((?:\[.+?]|\\.|[^/\\\r\n])+)\/([gimyu]{0,5})/g,
extract: function(match) {
var body = match[1],
flags = match[2];
return flags ? [body, flags] : body;
}
}
}
},
infix: {
'=': 40,
'<>': 40,
like: 40,
// TODO '**': 90, // exponent
between: {
priority: 70,
led: function(left) {
var me = this,
parser = me.parser;
me.arity = 'between';
me.operand = left;
me.low = parser.parseExpression(parser.symbols.and.priority);
parser.advance('&&');
me.high = parser.parseExpression(80);
return me;
}
},
'in': {
priority: 40,
led: function(left) {
var me = this,
parser = me.parser;
parser.advance('(');
me.arity = 'binary';
me.lhs = left;
me.rhs = {
arity: 'literal',
value: parser.parseList(),
is: LIST
};
parser.advance(')');
return me;
}
}
},
infixRight: {
'and': 30,
'or': 30
},
prefix: {
not: 0
},
parse: function() {
var expr = this.parseExpression();
return this.convert(expr);
},
privates: {
opCodes: {
binary: {
'=': 'eq',
'>': 'gt',
'<': 'lt',
'>=': 'ge',
'<=': 'le',
'!=': 'ne',
'<>': 'ne',
'+': 'add',
'/': 'div',
'*': 'mul',
'-': 'sub'
},
unary: {
'-': 'neg',
'!': 'not'
}
},
convert: function(node) {
var me = this,
arity = node.arity,
is = node.is,
name = node.name,
opCodes = me.opCodes,
value = node.value,
exprs, lhs, rhs, ret;
switch (arity) {
case 'between':
ret = {
type: 'between',
on: [
me.convert(node.operand),
me.convert(node.low),
me.convert(node.high)
]
};
break;
case 'ident':
ret = {
type: 'id',
value: value
};
break;
case 'invoke':
ret = {
type: 'fn',
fn: node.operand.value,
args: me.convertArray(node.args)
};
break;
case 'unary':
ret = {
type: opCodes.unary[value],
on: me.convert(node.operand)
};
break;
case 'binary':
if (name === 'and' || name === 'or') {
lhs = me.convert(node.lhs);
rhs = me.convert(node.rhs);
if (rhs.type === name) {
exprs = rhs.on;
exprs.unshift(lhs);
}
else {
exprs = [lhs, rhs];
}
ret = {
type: name,
on: exprs
};
}
else {
if (value === 'or') {
value = '||';
}
ret = {
type: opCodes.binary[value] || name,
on: [
me.convert(node.lhs),
me.convert(node.rhs)
]
};
if (name === 'like') {
ret.on[1] = me.likeToRe(ret.on[1], node.rhs.at);
}
}
break;
case 'literal':
if (is.string || is.number || is.boolean) {
ret = value;
}
else {
ret = {
type: is.type,
value: value
};
if (is.list) {
ret.value = me.convertArray(value);
}
else if (is.regexp && typeof value !== 'string') {
ret.value = value[0];
ret.flags = value[1];
}
}
break;
}
if (ret && typeof ret === 'object' && !ret.type) {
ret.type = arity;
}
return ret;
},
convertArray: function(array) {
var ret = [],
i = array.length;
for (; i-- > 0; /* empty */) {
ret[i] = this.convert(array[i]);
}
return ret;
},
likeToRe: function(node, at) {
if (typeof node === 'string') {
node = {
type: 'string',
value: node
};
}
else if (node.type === 'regexp') {
return node;
}
// eslint-disable-next-line vars-on-top
var specialChars = this.specialChars || (QueryParser.prototype.specialChars =
Ext.Array.toMap('.+*?^$=!|:-<>[](){}\\'.split(''))),
like = node.value,
n = like.length,
re = '',
simple = true,
escape, c, i, start;
outer: for (i = 0; i < n; ++i) {
c = like[i];
if (!escape) {
if (c === '\\') {
escape = c;
continue;
}
if (c === '*' || c === '%') {
re += '.*';
simple = false;
continue;
}
if (c === '?' || c === '_') {
re += '.';
simple = false;
continue;
}
// Some SQL-dialects (TSQL) support charsets: name like '[Bb]ob'
if (c === '[') {
re += c;
simple = false;
start = i;
while (++i < n) {
c = like[i];
if (escape) {
re += escape + c;
escape = 0;
}
else if (c === '\\') {
escape = c;
}
else {
re += c;
if (c === ']') {
continue outer;
}
}
}
// If we fall out of the while loop we never found the close
// of the charset... so throw a parse error
this.syntaxError(start + (node.at || at || 0),
'Incomplete character set');
}
}
escape = 0;
if (specialChars[c]) {
re += '\\';
}
re += c;
}
node.re = re || '.*';
// Assume most users (at least those that don't include
// a SQL wildcard) don't know about SQL wildcards in LIKE
// operators... If there are no wildcards present, assume
// the user wants a case-insensitive, substring match.
if (simple) {
node.flags = 'i';
}
else {
node.re = '^' + re + '$';
}
return node;
},
parseList: function() {
var me = this,
list = [];
do {
if (list.length) {
me.advance(); // the ','
}
list.push(me.parseExpression());
}
while (me.token.id === ',');
return list;
}
}
};
});