不要怂,就是干,撸起袖子干!

Commit 0af04063 by Yoni Jah Committed by Jan Aagaard Meier

fix(query-generator) Simplify where item query (#8068)

* refactor(abstruct.query-generator): Reduce complexity of abstruct query-generator whereItemQuery by

whereItemQuery had cyclic complexity of 85 reduced to 26 to make logic a bit easier to follow.
Logic is almost identical but at places where logic looked like an obvoius error it was tweeked, like -
	_traverseJSON had weird logic if the item was plain object -
		cast wast set by either path (ok) or the first value of the first property of an object (doesn't make sense)
		where value passed into whereItemQuery was in the main function scope causing it to accumulate properties on each iteration (doesn't make sense)
All test are passing with no issues but since there were minor logic changes there might be some edge cases needed to be addressed.
Though it's more resonable to assume changes will fix bugs related to those edge cases than cause them

* fix(abstract.query-generator): Fix discrepancies between postgres and sqlite casting and handling of

postgres and sqlite handle casting and JSONB formats a bit diffrently yet some previous commits
didn't took this changes into consideration when handling casting and didn't test casting properly

* fix(abstract.query-generator): Fix issue where not properly treating  plain js object values for JSON column

Fixes #3824 (again)
1 parent 0f8417a2
...@@ -1944,411 +1944,422 @@ const QueryGenerator = { ...@@ -1944,411 +1944,422 @@ const QueryGenerator = {
return items.length && items.filter(item => item && item.length).join(binding) || ''; return items.length && items.filter(item => item && item.length).join(binding) || '';
}, },
whereItemQuery(key, value, options) {
options = options || {}; OperatorsAliasMap: {
'ne': '$ne',
'in': '$in',
'not': '$not',
'notIn': '$notIn',
'gte': '$gte',
'gt': '$gt',
'lte': '$lte',
'lt': '$lt',
'like': '$like',
'ilike': '$iLike',
'$ilike': '$iLike',
'nlike': '$notLike',
'$notlike': '$notLike',
'notilike': '$notILike',
'..': '$between',
'between': '$between',
'!..': '$notBetween',
'notbetween': '$notBetween',
'nbetween': '$notBetween',
'overlap': '$overlap',
'&&': '$overlap',
'@>': '$contains',
'<@': '$contained'
},
let binding; OperatorMap: {
let outerBinding; $eq: '=',
let comparator = '='; $ne: '!=',
let field = options.field || options.model && options.model.rawAttributes && options.model.rawAttributes[key] || options.model && options.model.fieldRawAttributesMap && options.model.fieldRawAttributesMap[key]; $gte: '>=',
let fieldType = field && field.type || options.type; $gt: '>',
$lte: '<=',
$lt: '<',
$not: 'IS NOT',
$is: 'IS',
$like: 'LIKE',
$notLike: 'NOT LIKE',
$iLike: 'ILIKE',
$notILike: 'NOT ILIKE',
$regexp: '~',
$notRegexp: '!~',
$iRegexp: '~*',
$notIRegexp: '!~*',
$between: 'BETWEEN',
$notBetween: 'NOT BETWEEN',
$overlap: '&&',
$contains: '@>',
$contained: '<@',
$adjacent: '-|-',
$strictLeft: '<<',
$strictRight: '>>',
$noExtendRight: '&<',
$noExtendLeft: '&>',
$in : 'IN',
$notIn: 'NOT IN',
$any: 'ANY',
$all: 'ALL',
$and: ' AND ',
$or: ' OR ',
$col: 'COL',
$raw: 'DEPRECATED' //kept here since we still throw an explicit error if operator being used
},
whereItemQuery(key, value, options) {
options = options || {};
if (key && typeof key === 'string' && key.indexOf('.') !== -1 && options.model) { if (key && typeof key === 'string' && key.indexOf('.') !== -1 && options.model) {
if (options.model.rawAttributes[key.split('.')[0]] && options.model.rawAttributes[key.split('.')[0]].type instanceof DataTypes.JSON) { const keyParts = key.split('.');
field = options.model.rawAttributes[key.split('.')[0]]; if (options.model.rawAttributes[keyParts[0]] && options.model.rawAttributes[keyParts[0]].type instanceof DataTypes.JSON) {
fieldType = field.type; const tmp = {};
const tmp = value; const field = options.model.rawAttributes[keyParts[0]];
value = {}; Dottie.set(tmp, keyParts.slice(1), value);
return this.whereItemQuery(field.field || keyParts[0], tmp, Object.assign({field}, options));
Dottie.set(value, key.split('.').slice(1), tmp); }
key = field.field || key.split('.')[0]; }
}
}
const comparatorMap = {
$eq: '=',
$ne: '!=',
$gte: '>=',
$gt: '>',
$lte: '<=',
$lt: '<',
$not: 'IS NOT',
$is: 'IS',
$like: 'LIKE',
$notLike: 'NOT LIKE',
$iLike: 'ILIKE',
$notILike: 'NOT ILIKE',
$regexp: '~',
$notRegexp: '!~',
$iRegexp: '~*',
$notIRegexp: '!~*',
$between: 'BETWEEN',
$notBetween: 'NOT BETWEEN',
$overlap: '&&',
$contains: '@>',
$contained: '<@',
$adjacent: '-|-',
$strictLeft: '<<',
$strictRight: '>>',
$noExtendRight: '&<',
$noExtendLeft: '&>'
};
// Maintain BC const field = this._findField(key, options);
const aliasMap = { const fieldType = field && field.type || options.type;
'ne': '$ne',
'in': '$in',
'not': '$not',
'notIn': '$notIn',
'gte': '$gte',
'gt': '$gt',
'lte': '$lte',
'lt': '$lt',
'like': '$like',
'ilike': '$iLike',
'$ilike': '$iLike',
'nlike': '$notLike',
'$notlike': '$notLike',
'notilike': '$notILike',
'..': '$between',
'between': '$between',
'!..': '$notBetween',
'notbetween': '$notBetween',
'nbetween': '$notBetween',
'overlap': '$overlap',
'&&': '$overlap',
'@>': '$contains',
'<@': '$contained'
};
key = aliasMap[key] || key; const isPlainObject = _.isPlainObject(value);
if (_.isPlainObject(value)) { const isArray = !isPlainObject && Array.isArray(value);
_.forOwn(value, (item, key) => { key = this.OperatorsAliasMap[key] || key;
if (aliasMap[key]) { if (isPlainObject) {
value[aliasMap[key]] = item; this._replaceAliases(value);
delete value[key];
}
});
} }
const valueKeys = isPlainObject && _.keys(value);
if (key === undefined) { if (key === undefined) {
if (typeof value === 'string') { if (typeof value === 'string') {
return value; return value;
} }
if (_.isPlainObject(value) && _.size(value) === 1) { if (isPlainObject && valueKeys.length === 1) {
key = Object.keys(value)[0]; return this.whereItemQuery(valueKeys[0], value[valueKeys[0]], options);
value = _.values(value)[0];
} }
} }
if (value && value instanceof Utils.SequelizeMethod && !(key !== undefined && value instanceof Utils.Fn)) { if (!value) {
return this._joinKeyValue(key, this.escape(value, field), value === null ? this.OperatorMap.$is : this.OperatorMap.$eq, options.prefix);
}
if (value instanceof Utils.SequelizeMethod && !(key !== undefined && value instanceof Utils.Fn)) {
return this.handleSequelizeMethod(value); return this.handleSequelizeMethod(value);
} }
// Convert where: [] to $and if possible, else treat as literal/replacements // Convert where: [] to $and if possible, else treat as literal/replacements
if (key === undefined && Array.isArray(value)) { if (key === undefined && isArray) {
if (Utils.canTreatArrayAsAnd(value)) { if (Utils.canTreatArrayAsAnd(value)) {
key = '$and'; key = '$and';
} else { } else {
throw new Error('Support for literal replacements in the `where` object has been removed.'); throw new Error('Support for literal replacements in the `where` object has been removed.');
} }
} }
// OR/AND/NOT grouping logic
if (key === '$or' || key === '$and' || key === '$not') { if (key === '$or' || key === '$and' || key === '$not') {
binding = key === '$or' ?' OR ' : ' AND '; return this._whereGroupBind(key, value, options);
outerBinding = ''; }
if (key === '$not') outerBinding = 'NOT ';
if (Array.isArray(value)) { if (value.$or) {
value = value.map(item => { return this._whereBind(this.OperatorMap.$or, key, value.$or, options);
let itemQuery = this.whereItemsQuery(item, options, ' AND '); }
if ((Array.isArray(item) || _.isPlainObject(item)) && _.size(item) > 1) {
itemQuery = '('+itemQuery+')';
}
return itemQuery;
}).filter(item => item && item.length);
// $or: [] should return no data. if (value.$and) {
// $not of no restriction should also return no data return this._whereBind(this.OperatorMap.$and, key, value.$and, options);
if ((key === '$or' || key === '$not') && value.length === 0) { }
return '0 = 1';
}
return value.length ? outerBinding + '('+value.join(binding)+')' : undefined; if (isArray && fieldType instanceof DataTypes.ARRAY) {
} else { return this._joinKeyValue(key, this.escape(value, field), this.OperatorMap.$eq, options.prefix);
value = this.whereItemsQuery(value, options, binding); }
if ((key === '$or' || key === '$not') && !value) { if (isPlainObject && fieldType instanceof DataTypes.JSON && options.json !== false) {
return '0 = 1'; return this._whereJSON(key, value, options);
} }
// If multiple keys we combine the different logic conditions
if (isPlainObject && valueKeys.length > 1) {
return this._whereBind(this.OperatorMap.$and, key, value, options);
}
return value ? outerBinding + '('+value+')' : undefined; if (isArray) {
return this._whereParseSingleValueObject(key, field, '$in', value, options);
}
if (isPlainObject && this.OperatorMap[valueKeys[0]]) {
if (this.OperatorMap[valueKeys[0]]) {
return this._whereParseSingleValueObject(key, field, valueKeys[0], value[valueKeys[0]], options);
} else {
return this._whereParseSingleValueObject(key, field, this.OperatorMap.$eq, value, options);
} }
} }
if (value && (value.$or || value.$and)) { return this._joinKeyValue(key, this.escape(value, field), this.OperatorMap.$eq, options.prefix);
binding = value.$or ? ' OR ' : ' AND '; },
value = value.$or || value.$and;
if (_.isPlainObject(value)) { _findField(key, options) {
value = _.reduce(value, (result, _value, key) => { if (options.field) {
result.push(_.zipObject([key], [_value])); return options.field;
return result; }
}, []);
}
value = value.map(_value => this.whereItemQuery(key, _value, options)).filter(item => item && item.length); if (options.model && options.model.rawAttributes && options.model.rawAttributes[key]) {
return options.model.rawAttributes[key];
}
return value.length ? '('+value.join(binding)+')' : undefined; if (options.model && options.model.fieldRawAttributesMap && options.model.fieldRawAttributesMap[key]) {
return options.model.fieldRawAttributesMap[key];
} }
},
if (_.isPlainObject(value) && fieldType instanceof DataTypes.JSON && options.json !== false) { _replaceAliases(obj) {
const items = []; _.forOwn(obj, (item, prop) => {
const traverse = (prop, item, path) => { if (this.OperatorsAliasMap[prop]) {
const where = {}; obj[this.OperatorsAliasMap[prop]] = item;
let cast; delete obj[prop];
}
});
},
if (path[path.length - 1].indexOf('::') > -1) { // OR/AND/NOT grouping logic
const tmp = path[path.length - 1].split('::'); _whereGroupBind(key, value, options) {
cast = tmp[1]; const binding = key === '$or' ? this.OperatorMap.$or : this.OperatorMap.$and;
path[path.length - 1] = tmp[0]; const outerBinding = key === '$not' ? 'NOT ': '';
if (Array.isArray(value)) {
value = value.map(item => {
let itemQuery = this.whereItemsQuery(item, options, this.OperatorMap.$and);
if (itemQuery && itemQuery.length && (Array.isArray(item) || _.isPlainObject(item)) && _.size(item) > 1) {
itemQuery = '('+itemQuery+')';
} }
return itemQuery;
}).filter(item => item && item.length);
let baseKey = this.quoteIdentifier(key); value = value.length && value.join(binding);
} else {
value = this.whereItemsQuery(value, options, binding);
}
// Op.or: [] should return no data.
// Op.not of no restriction should also return no data
if ((key === '$or' || key === '$not') && !value) {
return '0 = 1';
}
if (options.prefix) { return value ? outerBinding + '('+value+')' : undefined;
if (options.prefix instanceof Utils.Literal) { },
baseKey = `${this.handleSequelizeMethod(options.prefix)}.${baseKey}`;
} else {
baseKey = `${this.quoteTable(options.prefix)}.${baseKey}`;
}
}
baseKey = this.jsonPathExtractionQuery(baseKey, path); _whereBind(binding, key, value, options) {
if (_.isPlainObject(value)) {
value = _.map(value, (item, prop) => this.whereItemQuery(key, {[prop] : item}, options));
} else {
value = value.map(item => this.whereItemQuery(key, item, options));
}
const castKey = item => { value = value.filter(item => item && item.length);
const key = baseKey;
if (!cast) { return value.length ? '('+value.join(binding)+')' : undefined;
if (typeof item === 'number') { },
cast = 'double precision';
} else if (item instanceof Date) {
cast = 'timestamptz';
} else if (typeof item === 'boolean') {
cast = 'boolean';
}
}
if (cast) { _whereJSON(key, value, options) {
return this.handleSequelizeMethod(new Utils.Cast(new Utils.Literal(key), cast)); const items = [];
} let baseKey = this.quoteIdentifier(key);
if (options.prefix) {
if (options.prefix instanceof Utils.Literal) {
baseKey = `${this.handleSequelizeMethod(options.prefix)}.${baseKey}`;
} else {
baseKey = `${this.quoteTable(options.prefix)}.${baseKey}`;
}
}
_.forOwn(value, (item, prop) => {
if (prop.indexOf('$') === 0) {
const where = {};
where[prop] = value[prop];
items.push(this.whereItemQuery(key, where, _.assign({}, options, {json: false})));
return;
}
this._traverseJSON(items, baseKey, prop, item, [prop]);
});
return key; const result = items.join(this.OperatorMap.$and);
}; return items.length > 1 ? '('+result+')' : result;
},
if (_.isPlainObject(item)) { _traverseJSON(items, baseKey, prop, item, path) {
_.forOwn(item, (item, prop) => { let cast;
if (prop.indexOf('$') === 0) {
where[prop] = item;
const key = castKey(item);
items.push(this.whereItemQuery(new Utils.Literal(key), where/*, _.pick(options, 'prefix')*/)); if (path[path.length - 1].indexOf('::') > -1) {
} else { const tmp = path[path.length - 1].split('::');
traverse(prop, item, path.concat([prop])); cast = tmp[1];
} path[path.length - 1] = tmp[0];
}); }
} else {
where.$eq = item;
const key = castKey(item);
items.push(this.whereItemQuery(new Utils.Literal(key), where/*, _.pick(options, 'prefix')*/)); const pathKey = this.jsonPathExtractionQuery(baseKey, path);
}
};
_.forOwn(value, (item, prop) => { if (_.isPlainObject(item)) {
if (prop.indexOf('$') === 0) { _.forOwn(item, (value, itemProp) => {
const where = {}; if (itemProp.indexOf('$') === 0) {
where[prop] = item; items.push(this.whereItemQuery(this._castKey(pathKey, value, cast), {[itemProp]: value}));
items.push(this.whereItemQuery(key, where, _.assign({}, options, {json: false})));
return; return;
} }
this._traverseJSON(items, baseKey, itemProp, value, path.concat([itemProp]));
traverse(prop, item, [prop]);
}); });
const result = items.join(' AND '); return;
return items.length > 1 ? '('+result+')' : result;
} }
// If multiple keys we combine the different logic conditions items.push(this.whereItemQuery(this._castKey(pathKey, item, cast), {$eq: item}));
if (_.isPlainObject(value) && Object.keys(value).length > 1) { },
const items = [];
_.forOwn(value, (item, logic) => {
const where = {};
where[logic] = item;
items.push(this.whereItemQuery(key, where, options));
});
return '('+items.join(' AND ')+')'; _castKey(key, value, cast) {
cast = cast || this._getJsonCast(Array.isArray(value) ? value[0] : value);
if (cast) {
return new Utils.Literal(this.handleSequelizeMethod(new Utils.Cast(new Utils.Literal(key), cast)));
} }
// Do [] to $in/$notIn normalization return new Utils.Literal(key);
if (value && (!fieldType || !(fieldType instanceof DataTypes.ARRAY))) { },
if (Array.isArray(value)) {
value = {
$in: value
};
} else if (value && Array.isArray(value.$not)) {
value.$notIn = value.$not;
delete value.$not;
}
}
// normalize $not: non-bool|non-null to $ne _getJsonCast(value) {
if (value && typeof value.$not !== 'undefined' && [null, true, false].indexOf(value.$not) < 0) { if (typeof value === 'number') {
value.$ne = value.$not; return 'double precision';
delete value.$not; }
if (value instanceof Date) {
return 'timestamptz';
} }
if (typeof value === 'boolean') {
return 'boolean';
}
return;
},
// Setup keys and comparators _joinKeyValue(key, value, comparator, prefix) {
if (Array.isArray(value) && fieldType instanceof DataTypes.ARRAY) { if (!key) {
value = this.escape(value, field); return value;
} else if (value && (value.$in || value.$notIn)) { }
comparator = 'IN'; if (comparator === undefined) {
if (value.$notIn) comparator = 'NOT IN'; throw new Error(`${key} and ${value} has no comperator`);
}
key = this._getSafeKey(key, prefix);
return [key, value].join(' '+comparator+' ');
},
if ((value.$in || value.$notIn) instanceof Utils.Literal) { _getSafeKey(key, prefix) {
value = (value.$in || value.$notIn).val; if (key instanceof Utils.SequelizeMethod) {
} else if ((value.$in || value.$notIn).length) { key = this.handleSequelizeMethod(key);
value = '('+(value.$in || value.$notIn).map(item => this.escape(item)).join(', ')+')'; return this._prefixKey(this.handleSequelizeMethod(key), prefix);
} else { }
if (value.$in) {
value = '(NULL)';
} else {
return '';
}
}
} else if (value && (value.$any || value.$all)) {
comparator = value.$any ? '= ANY' : '= ALL';
if (value.$any && value.$any.$values || value.$all && value.$all.$values) {
value = '(VALUES '+(value.$any && value.$any.$values || value.$all && value.$all.$values).map(value => '('+this.escape(value)+')').join(', ')+')';
} else {
value = '('+this.escape(value.$any || value.$all, field)+')';
}
} else if (value && (value.$between || value.$notBetween)) {
comparator = 'BETWEEN';
if (value.$notBetween) comparator = 'NOT BETWEEN';
value = (value.$between || value.$notBetween).map(item => this.escape(item)).join(' AND '); if (Utils.isColString(key)) {
} else if (value && value.$raw) { key = key.substr(1, key.length - 2).split('.');
throw new Error('The `$raw` where property is no longer supported. Use `sequelize.literal` instead.');
} else if (value && value.$col) {
value = value.$col.split('.');
if (value.length > 2) { if (key.length > 2) {
value = [ key = [
// join the tables by -> to match out internal namings // join the tables by -> to match out internal namings
value.slice(0, -1).join('->'), key.slice(0, -1).join('->'),
value[value.length - 1] key[key.length - 1]
]; ];
} }
value = value.map(identifier => this.quoteIdentifier(identifier)).join('.'); return key.map(identifier => this.quoteIdentifier(identifier)).join('.');
} else { }
let escapeValue = true;
const escapeOptions = {}; return this._prefixKey(this.quoteIdentifier(key), prefix);
},
if (_.isPlainObject(value)) {
_.forOwn(value, (item, key) => {
if (comparatorMap[key]) {
comparator = comparatorMap[key];
value = item;
if (_.isPlainObject(value) && value.$any) {
comparator += ' ANY';
escapeOptions.isList = true;
value = value.$any;
} else if (_.isPlainObject(value) && value.$all) {
comparator += ' ALL';
escapeOptions.isList = true;
value = value.$all;
} else if (value && value.$col) {
escapeValue = false;
value = this.whereItemQuery(null, value);
}
}
});
}
if (comparator === '=' && value === null) { _prefixKey(key, prefix) {
comparator = 'IS'; if (prefix) {
} else if (comparator === '!=' && value === null) { if (prefix instanceof Utils.Literal) {
comparator = 'IS NOT'; return [this.handleSequelizeMethod(prefix), key].join('.');
} }
if (comparator.indexOf('~') !== -1) { return [this.quoteTable(prefix), key].join('.');
escapeValue = false; }
return key;
},
_whereParseSingleValueObject(key, field, prop, value, options) {
if (prop === '$not') {
if (Array.isArray(value)) {
prop = '$notIn';
} else if ([null, true, false].indexOf(value) < 0) {
prop = '$ne';
} }
}
if (this._dialect.name === 'mysql') { let comparator = this.OperatorMap[prop] || this.OperatorMap.$eq;
if (comparator === '~') {
comparator = 'REGEXP'; switch (prop) {
} else if (comparator === '!~') { case '$in':
comparator = 'NOT REGEXP'; case '$notIn':
if (value instanceof Utils.Literal) {
return this._joinKeyValue(key, value.val, comparator, options.prefix);
} }
}
escapeOptions.acceptStrings = comparator.indexOf('LIKE') !== -1; if (value.length) {
escapeOptions.acceptRegExp = comparator.indexOf('~') !== -1 || comparator.indexOf('REGEXP') !== -1; return this._joinKeyValue(key, `(${value.map(item => this.escape(item)).join(', ')})`, comparator, options.prefix);
}
if (escapeValue) { if (comparator === this.OperatorMap.$in) {
value = this.escape(value, field, escapeOptions); return this._joinKeyValue(key, '(NULL)', comparator, options.prefix);
}
// if ANY or ALL is used with like, add parentheses to generate correct query return '';
if (escapeOptions.acceptStrings && ( case '$any':
comparator.indexOf('ANY') > comparator.indexOf('LIKE') || case '$all':
comparator.indexOf('ALL') > comparator.indexOf('LIKE') comparator = `${this.OperatorMap.$eq} ${comparator}`;
)) { if (value.$values) {
value = '(' + value + ')'; return this._joinKeyValue(key, `(VALUES ${value.$values.map(item => `(${this.escape(item)})`).join(', ')})`, comparator, options.prefix);
} }
} else if (escapeOptions.acceptRegExp) {
value = '\'' + value + '\'';
}
}
if (key) { return this._joinKeyValue(key, `(${this.escape(value, field)})`, comparator, options.prefix);
let prefix = true; case '$between':
if (key instanceof Utils.SequelizeMethod) { case '$notBetween':
key = this.handleSequelizeMethod(key); return this._joinKeyValue(key, `${this.escape(value[0])} AND ${this.escape(value[1])}`, comparator, options.prefix);
} else if (Utils.isColString(key)) { case '$raw':
key = key.substr(1, key.length - 2).split('.'); throw new Error('The `$raw` where property is no longer supported. Use `sequelize.literal` instead.');
case '$col':
comparator = this.OperatorMap.$eq;
value = value.split('.');
if (key.length > 2) { if (value.length > 2) {
key = [ value = [
// join the tables by -> to match out internal namings // join the tables by -> to match out internal namings
key.slice(0, -1).join('->'), value.slice(0, -1).join('->'),
key[key.length - 1] value[value.length - 1]
]; ];
} }
key = key.map(identifier => this.quoteIdentifier(identifier)).join('.'); return this._joinKeyValue(key, value.map(identifier => this.quoteIdentifier(identifier)).join('.'), comparator, options.prefix);
prefix = false; }
} else {
key = this.quoteIdentifier(key);
}
if (options.prefix && prefix) { const escapeOptions = {
if (options.prefix instanceof Utils.Literal) { acceptStrings: comparator.indexOf(this.OperatorMap.$like) !== -1
key = [this.handleSequelizeMethod(options.prefix), key].join('.'); };
} else {
key = [this.quoteTable(options.prefix), key].join('.'); if (_.isPlainObject(value)) {
} if (value.$col) {
return this._joinKeyValue(key, this.whereItemQuery(null, value), comparator, options.prefix);
}
if (value.$any) {
escapeOptions.isList = true;
return this._joinKeyValue(key, `(${this.escape(value.$any, field, escapeOptions)})`, `${comparator} ${this.OperatorMap.$any}`, options.prefix);
}
if (value.$all) {
escapeOptions.isList = true;
return this._joinKeyValue(key, `(${this.escape(value.$all, field, escapeOptions)})`, `${comparator} ${this.OperatorMap.$all}`, options.prefix);
} }
return [key, value].join(' '+comparator+' ');
} }
return value;
if (comparator.indexOf(this.OperatorMap.$regexp) !== -1) {
return this._joinKeyValue(key, `'${value}'`, comparator, options.prefix);
}
if (value === null && comparator === this.OperatorMap.$eq) {
return this._joinKeyValue(key, this.escape(value, field, escapeOptions), this.OperatorMap.$is, options.prefix);
} else if (value === null && this.OperatorMap.$ne) {
return this._joinKeyValue(key, this.escape(value, field, escapeOptions), this.OperatorMap.$not, options.prefix);
}
return this._joinKeyValue(key, this.escape(value, field, escapeOptions), comparator, options.prefix);
}, },
/* /*
......
...@@ -7,6 +7,11 @@ const QueryGenerator = { ...@@ -7,6 +7,11 @@ const QueryGenerator = {
__proto__: AbstractQueryGenerator, __proto__: AbstractQueryGenerator,
dialect: 'mysql', dialect: 'mysql',
OperatorMap: Object.assign({}, AbstractQueryGenerator.OperatorMap, {
$regexp: 'REGEXP',
$notRegexp: 'NOT REGEXP'
}),
createSchema() { createSchema() {
return 'SHOW TABLES'; return 'SHOW TABLES';
}, },
......
...@@ -157,6 +157,44 @@ const QueryGenerator = { ...@@ -157,6 +157,44 @@ const QueryGenerator = {
return `json_extract(${quotedColumn}, ${pathStr})`; return `json_extract(${quotedColumn}, ${pathStr})`;
}, },
//sqlite can't cast to datetime so we need to convert date values to their ISO strings
_traverseJSON(items, baseKey, prop, item, path) {
let cast;
if (path[path.length - 1].indexOf('::') > -1) {
const tmp = path[path.length - 1].split('::');
cast = tmp[1];
path[path.length - 1] = tmp[0];
}
const pathKey = this.jsonPathExtractionQuery(baseKey, path);
if (_.isPlainObject(item)) {
_.forOwn(item, (value, itemProp) => {
if (itemProp.indexOf('$') === 0) {
if (value instanceof Date) {
value = value.toISOString();
} else if (Array.isArray(value) && value[0] instanceof Date) {
value = value.map(val => val.toISOString());
}
items.push(this.whereItemQuery(this._castKey(pathKey, value, cast), {[itemProp]: value}));
return;
}
this._traverseJSON(items, baseKey, itemProp, value, path.concat([itemProp]));
});
return;
}
if (item instanceof Date) {
item = item.toISOString();
} else if (Array.isArray(item) && item[0] instanceof Date) {
item = item.map(val => val.toISOString());
}
items.push(this.whereItemQuery(this._castKey(pathKey, item, cast), {$eq: item}));
},
handleSequelizeMethod(smth, tableName, factory, options, prepend) { handleSequelizeMethod(smth, tableName, factory, options, prepend) {
if (smth instanceof Utils.Json) { if (smth instanceof Utils.Json) {
// Parse nested object // Parse nested object
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
const chai = require('chai'), const chai = require('chai'),
Sequelize = require('../../../index'), Sequelize = require('../../../index'),
Promise = Sequelize.Promise, Promise = Sequelize.Promise,
moment = require('moment'),
expect = chai.expect, expect = chai.expect,
Support = require(__dirname + '/../support'), Support = require(__dirname + '/../support'),
DataTypes = require(__dirname + '/../../../lib/data-types'), DataTypes = require(__dirname + '/../../../lib/data-types'),
...@@ -249,6 +250,96 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -249,6 +250,96 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}); });
}); });
it('should be possible to query dates with array operators', function() {
const now = moment().toDate();
const before = moment().subtract(1, 'day').toDate();
const after = moment().add(1, 'day').toDate();
return Promise.join(
this.Event.create({
json: {
user: 'Homer',
lastLogin: now
}
})
).then(() => {
return this.Event.findAll({
where: {
json: {
lastLogin: now
}
}
}).then(events => {
const event = events[0];
expect(events.length).to.equal(1);
expect(event.get('json')).to.eql({
user: 'Homer',
lastLogin: now.toISOString()
});
});
}).then(() => {
return this.Event.findAll({
where: {
json: {
lastLogin: {$between: [before, after]}
}
},
logging: console.log.bind(console)
}).then(events => {
const event = events[0];
expect(events.length).to.equal(1);
expect(event.get('json')).to.eql({
user: 'Homer',
lastLogin: now.toISOString()
});
});
});
});
it('should be possible to query a boolean with array operators', function() {
return Promise.join(
this.Event.create({
json: {
user: 'Homer',
active: true
}
})
).then(() => {
return this.Event.findAll({
where: {
json: {
active: true
}
}
}).then(events => {
const event = events[0];
expect(events.length).to.equal(1);
expect(event.get('json')).to.eql({
user: 'Homer',
active: true
});
});
}).then(() => {
return this.Event.findAll({
where: {
json: {
active: {$in: [true, false]}
}
}
}).then(events => {
const event = events[0];
expect(events.length).to.equal(1);
expect(event.get('json')).to.eql({
user: 'Homer',
active: true
});
});
});
});
it('should be possible to query a nested integer value', function() { it('should be possible to query a nested integer value', function() {
return Promise.join( return Promise.join(
this.Event.create({ this.Event.create({
......
...@@ -789,6 +789,32 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -789,6 +789,32 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
testsql('data', { testsql('data', {
nested: { nested: {
$in: [1, 2]
}
}, {
field: {
type: new DataTypes.JSONB()
}
}, {
postgres: "CAST((\"data\"#>>'{nested}') AS DOUBLE PRECISION) IN (1, 2)",
sqlite: "CAST(json_extract(`data`, '$.nested') AS DOUBLE PRECISION) IN (1, 2)"
});
testsql('data', {
nested: {
$between: [1, 2]
}
}, {
field: {
type: new DataTypes.JSONB()
}
}, {
postgres: "CAST((\"data\"#>>'{nested}') AS DOUBLE PRECISION) BETWEEN 1 AND 2",
sqlite: "CAST(json_extract(`data`, '$.nested') AS DOUBLE PRECISION) BETWEEN 1 AND 2"
});
testsql('data', {
nested: {
attribute: 'value', attribute: 'value',
prop: { prop: {
$ne: 'None' $ne: 'None'
...@@ -821,6 +847,18 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -821,6 +847,18 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
sqlite: "(json_extract(`User`.`data`, '$.name.last') = 'Simpson' AND json_extract(`User`.`data`, '$.employment') != 'None')" sqlite: "(json_extract(`User`.`data`, '$.name.last') = 'Simpson' AND json_extract(`User`.`data`, '$.employment') != 'None')"
}); });
testsql('data', {
price: 5,
name: 'Product'
}, {
field: {
type: new DataTypes.JSONB()
}
}, {
postgres: "(CAST((\"data\"#>>'{price}') AS DOUBLE PRECISION) = 5 AND (\"data\"#>>'{name}') = 'Product')",
sqlite: "(CAST(json_extract(`data`, '$.price') AS DOUBLE PRECISION) = 5 AND json_extract(`data`, '$.name') = 'Product')"
});
testsql('data.nested.attribute', 'value', { testsql('data.nested.attribute', 'value', {
model: { model: {
rawAttributes: { rawAttributes: {
...@@ -858,8 +896,8 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -858,8 +896,8 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
} }
} }
}, { }, {
postgres: "(\"data\"#>>'{nested,attribute}') IN (3, 7)", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) IN (3, 7)",
sqlite: "json_extract(`data`, '$.nested.attribute') IN (3, 7)" sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DOUBLE PRECISION) IN (3, 7)"
}); });
testsql('data', { testsql('data', {
...@@ -905,7 +943,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -905,7 +943,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
} }
}, { }, {
postgres: "CAST((\"data\"#>>'{nested,attribute}') AS TIMESTAMPTZ) > "+sql.escape(dt), postgres: "CAST((\"data\"#>>'{nested,attribute}') AS TIMESTAMPTZ) > "+sql.escape(dt),
sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DATETIME) > "+sql.escape(dt) sqlite: "json_extract(`data`, '$.nested.attribute') > " + sql.escape(dt.toISOString())
}); });
testsql('data', { testsql('data', {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!