/**
* This class is a filter that compiles from an SQL-like expression. For example:
*
* store.addFilter(new Ext.data.Query('name like "Bob" or age < 20'));
*
* Queries can also be assigned an `id`:
*
* store.addFilter(new Ext.data.Query({
* id: 'myquery',
* source: 'name like "Bob" or age < 20'
* ));
*
* ## Query Syntax
*
* The syntax for a query is SQL-like. The goal of the query syntax is to be as natural
* to end-users as possible and therefore does not exactly match JavaScript.
*
* ### Keyword Operators
*
* - `and` or `&&` - Logical AND
* - `or` or `||` - Logical OR
* - `like` - String containment or regex match
* - `in` - Set membership
* - `not` or `!` - Logical negation
* - `between` - Bounds check a value (`age between 18 and 99`)
*
* #### The `like` Operator
*
* There are several forms of `like`. The first uses a simple string on the right-side:
*
* name like "Bob"
*
* This expression evaluates as `true` if the `name` contains the substring `'Bob'`
* (ignoring case).
*
* The second form will be more typical of those familiar with SQL. It is when the
* right-side uses the SQL `%` or `_` wildcards (or the shell `*` or `?` wildcards) and/or
* character sets (such as `'[a-f]'` and `'[^abc]'`):
*
* name like "[BR]ob%"
*
* If any wildcards are used, the typical SQL meaning is assumed (strict match, including
* case).
*
* The right-side can also use shell wildcards `'*'` or `'?'` instead of SQL wildcards.
*
* These wildcards can be escaped with a backslash (`\`) character (the `escape` keyword
* is not supported).
*
* text like 'To be or not to be\?'
*
* The final form of `like` is when the right-side is a regular expression:
*
* name like /^Bob/i
*
* This form uses the `test()` method of the `RegExp` to match the value of `name`.
*
* #### The `in` Operator
*
* This operator accepts a parenthesized list of values and evaluates to `true` if the
* left-side value matches an item in the right-side list:
*
* name in ("Bob", 'Robert')
*
* ### Relational Operators
*
* - `<`
* - `<=`
* - `>`
* - `>=`
*
* ### Equality and Inequality
*
* - `=` - Equality after conversion (like `==` in JavaScript)
* - `==` or `===` - Strict equality (like `===` in JavaScript)
* - `!=` or `<>` - Inequality after conversion (like `!=` in JavaScript)
* - `!==` - Strict inequality (like `!==` in JavaScript)
*
* ### Helper Functions
*
* The following functions can be used in a query:
*
* - `abs(x)` - Absolute value of `x`
* - `avg(...)` - The average of all parameters.
* - `date(d)` - Converts the argument into a date.
* - `lower(s)` - The lower-case conversion of the given string.
* - `max(...)` - The maximum value of all parameters.
* - `min(...)` - The minimum value of all parameters.
* - `sum(...)` - The sum of all parameters.
* - `upper(s)` - The upper-case conversion of the given string.
*
* These functions are used as needed in queries, such as:
*
* upper(name) = 'BOB'
*
* @since 6.7.0
*/
Ext.define('Ext.data.Query', {
extend: 'Ext.util.BasicFilter',
mixins: [
'Ext.mixin.Factoryable',
'Ext.data.query.Compiler',
'Ext.data.query.Converter',
'Ext.data.query.Stringifier',
'Ext.mixin.Identifiable'
],
alias: 'query.default',
requires: [
'Ext.data.query.Parser'
],
config: {
/**
* @cfg {"ast"/"filters"/"query"} format
*/
format: 'ast',
/**
* @cfg {Object} functions
* This config contains the methods that will be made available to queries. To
* add a custom function:
*
* Ext.define('MyQuery', {
* extend: 'Ext.data.Query',
*
* functions: {
* round: function (x) {
* return Math.round(x);
* },
*
* // When a function name ends with "..." it is called
* // with the arguments as an array.
* //
* 'concat...': function (args) {
* return args.join('');
* }
* }
* });
*/
functions: {
cached: true,
$value: {
abs: function(arg) {
return Math.abs(arg);
},
'avg...': function(args) {
var count = 0,
sum = 0,
i = args.length,
v;
for (; i-- > 0; /* empty */) {
v = args[i];
if (v != null) {
sum += v;
++count;
}
}
return count ? sum / count : 0;
},
date: function(arg) {
return (arg instanceof Date) ? arg : Ext.Date.parse(arg);
},
lower: function(arg) {
return (arg == null) ? '' : String(arg).toLowerCase();
},
'max...': function(args) {
var ret = null,
i = args.length,
v;
for (; i-- > 0; /* empty */) {
v = args[i];
if (v != null) {
ret = (ret === null) ? v : (ret < v ? v : ret);
}
}
return ret;
},
'min...': function(args) {
var ret = null,
i = args.length,
v;
for (; i-- > 0; /* empty */) {
v = args[i];
if (v != null) {
ret = (ret === null) ? v : (ret < v ? ret : v);
}
}
return ret;
},
'sum...': function(args) {
var ret = null,
i = args.length,
v;
for (; i-- > 0; /* empty */) {
v = args[i];
if (v != null) {
ret = (ret === null) ? v : (ret + v);
}
}
return ret === null ? 0 : ret;
},
upper: function(arg) {
return (arg == null) ? '' : String(arg).toUpperCase();
}
}
},
/**
* @cfg {String} source
* The source text of this query. See {@link Ext.data.Query class documentation}
* for syntax details.
*/
source: ''
},
ast: null,
error: null,
generation: 0,
identifiablePrefix: 'ext-data-query-',
constructor: function(config) {
var me = this;
if (typeof config === 'string') {
config = {
source: config
};
}
me.id = (config && config.id) || me.generateAutoId();
// eslint-disable-next-line vars-on-top
var parser = Ext.data.query.Parser.fly();
me.symbols = parser.symbols;
parser.release();
me.callParent([ config ]);
},
filter: function(item) {
var me = this,
error = me.error;
if (error) {
throw error;
}
return !!me.fn(item);
},
/**
* This method should be called if the `ast` has been manipulated directly.
*/
refresh: function() {
++this.generation;
this.compile(); // assigns me.fn
},
serialize: function() {
var me = this,
format = me.getFormat(),
serializer = me.getSerializer(),
ret, serialized;
switch (format) {
case 'ast':
ret = me.ast;
if (serializer) {
ret = Ext.clone(ret);
}
break;
case 'filters':
ret = me.getFilters() || null;
break;
case 'query':
ret = me.toString();
break;
}
if (ret && serializer) {
serialized = serializer.call(this, ret);
if (serialized) {
ret = serialized;
}
}
return ret;
},
serializeTo: function(out) {
var filters = this.serialize(),
ret;
if (filters && filters.length) {
out.push.apply(out, filters);
ret = true;
}
return ret;
},
sync: function() {
var me = this,
fn = me.fn;
if (!fn || fn.generation !== me.generation) {
me.compile();
}
},
toString: function() {
var ast = this.ast;
return ast ? this.stringify(ast) : '';
},
//------------------------------------------------------------------------
// Configs
// format
//<debug>
validFormatsRe: /^(ast|filters|query)$/,
applyFormat: function(format) {
if (!this.validFormatsRe.test(format)) {
Ext.raise('Invalid query format');
}
return format;
},
//</debug>
// functions
applyFunctions: function(funcs) {
var ret = {},
vargsRe = this.vargsRe,
def, key, name;
for (key in funcs) {
def = {
fn: funcs[name = key],
vargs: vargsRe.test(key)
};
if (def.vargs) {
name = key.substr(0, key.length - 3); // remove '...'
}
ret[name.toLowerCase()] = def;
}
return ret;
},
// source
applySource: function(source) {
if (source) {
return source;
}
++this.generation;
this.ast = null;
this.compile(); // assigns me.fn
},
updateSource: function(source) {
var me = this,
parser = Ext.data.query.Parser.fly(source);
++me.generation;
try {
me.error = me.fn = null;
me.ast = parser.parse();
}
catch (e) {
me.error = e;
e.message = 'Failed to parse: ' + e.message;
throw e;
}
finally {
parser.release();
}
me.compile(); // assigns me.fn
},
//-------------------------------------------------------------------------
privates: {
operatorTypeMap: {
/* eslint-disable no-multi-spaces */
/* eslint-disable key-spacing */
// [ arity, JS-operator, Query-operator ]
and: ['binary', '&&', 'and' ],
or: ['binary', '||', 'or' ],
eq: ['binary', '==', '=' ],
ge: ['binary', '>=', null ],
gt: ['binary', '>', null ],
le: ['binary', '<=', null ],
lt: ['binary', '<', null ],
ne: ['binary', '!=', null ],
add: ['binary', '+', null ],
div: ['binary', '/', null ],
mul: ['binary', '*', null ],
sub: ['binary', '-', null ],
'in': ['binary', null, 'in' ],
like: ['binary', null, 'like' ],
seq: ['binary', '===', '==' ],
sne: ['binary', '!==', null ],
neg: ['unary', '-', null ],
not: ['unary', '!', null ]
/* eslint-enable no-multi-spaces */
/* eslint-enable key-spacing */
},
vargsRe: /\.\.\.$/,
getOperatorType: function(op) {
var map = this.operatorTypeMap,
key;
for (key in map) {
if (map[key][1] === op || map[key][2] === op) {
return key;
}
}
//<debug>
Ext.raise('Unrecognized filter operator: "' + op + '"');
//</debug>
return null;
}
}
});