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

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,29 +1944,34 @@ const QueryGenerator = { ...@@ -1944,29 +1944,34 @@ 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 || {};
let binding;
let outerBinding;
let comparator = '=';
let field = options.field || options.model && options.model.rawAttributes && options.model.rawAttributes[key] || options.model && options.model.fieldRawAttributesMap && options.model.fieldRawAttributesMap[key];
let fieldType = field && field.type || options.type;
if (key && typeof key === 'string' && key.indexOf('.') !== -1 && options.model) { OperatorsAliasMap: {
if (options.model.rawAttributes[key.split('.')[0]] && options.model.rawAttributes[key.split('.')[0]].type instanceof DataTypes.JSON) { 'ne': '$ne',
field = options.model.rawAttributes[key.split('.')[0]]; 'in': '$in',
fieldType = field.type; 'not': '$not',
const tmp = value; 'notIn': '$notIn',
value = {}; 'gte': '$gte',
'gt': '$gt',
Dottie.set(value, key.split('.').slice(1), tmp); 'lte': '$lte',
key = field.field || key.split('.')[0]; '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'
},
const comparatorMap = { OperatorMap: {
$eq: '=', $eq: '=',
$ne: '!=', $ne: '!=',
$gte: '>=', $gte: '>=',
...@@ -1992,132 +1997,170 @@ const QueryGenerator = { ...@@ -1992,132 +1997,170 @@ const QueryGenerator = {
$strictLeft: '<<', $strictLeft: '<<',
$strictRight: '>>', $strictRight: '>>',
$noExtendRight: '&<', $noExtendRight: '&<',
$noExtendLeft: '&>' $noExtendLeft: '&>',
}; $in : 'IN',
$notIn: 'NOT IN',
// Maintain BC $any: 'ANY',
const aliasMap = { $all: 'ALL',
'ne': '$ne', $and: ' AND ',
'in': '$in', $or: ' OR ',
'not': '$not', $col: 'COL',
'notIn': '$notIn', $raw: 'DEPRECATED' //kept here since we still throw an explicit error if operator being used
'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; whereItemQuery(key, value, options) {
if (_.isPlainObject(value)) { options = options || {};
_.forOwn(value, (item, key) => { if (key && typeof key === 'string' && key.indexOf('.') !== -1 && options.model) {
if (aliasMap[key]) { const keyParts = key.split('.');
value[aliasMap[key]] = item; if (options.model.rawAttributes[keyParts[0]] && options.model.rawAttributes[keyParts[0]].type instanceof DataTypes.JSON) {
delete value[key]; const tmp = {};
const field = options.model.rawAttributes[keyParts[0]];
Dottie.set(tmp, keyParts.slice(1), value);
return this.whereItemQuery(field.field || keyParts[0], tmp, Object.assign({field}, options));
} }
});
} }
const field = this._findField(key, options);
const fieldType = field && field.type || options.type;
const isPlainObject = _.isPlainObject(value);
const isArray = !isPlainObject && Array.isArray(value);
key = this.OperatorsAliasMap[key] || key;
if (isPlainObject) {
this._replaceAliases(value);
}
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 (value.$or) {
return this._whereBind(this.OperatorMap.$or, key, value.$or, options);
}
if (value.$and) {
return this._whereBind(this.OperatorMap.$and, key, value.$and, options);
}
if (isArray && fieldType instanceof DataTypes.ARRAY) {
return this._joinKeyValue(key, this.escape(value, field), this.OperatorMap.$eq, options.prefix);
}
if (isPlainObject && fieldType instanceof DataTypes.JSON && options.json !== false) {
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);
}
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);
}
}
return this._joinKeyValue(key, this.escape(value, field), this.OperatorMap.$eq, options.prefix);
},
_findField(key, options) {
if (options.field) {
return options.field;
}
if (options.model && options.model.rawAttributes && options.model.rawAttributes[key]) {
return options.model.rawAttributes[key];
}
if (options.model && options.model.fieldRawAttributesMap && options.model.fieldRawAttributesMap[key]) {
return options.model.fieldRawAttributesMap[key];
}
},
_replaceAliases(obj) {
_.forOwn(obj, (item, prop) => {
if (this.OperatorsAliasMap[prop]) {
obj[this.OperatorsAliasMap[prop]] = item;
delete obj[prop];
}
});
},
// OR/AND/NOT grouping logic
_whereGroupBind(key, value, options) {
const binding = key === '$or' ? this.OperatorMap.$or : this.OperatorMap.$and;
const outerBinding = key === '$not' ? 'NOT ': '';
if (Array.isArray(value)) { if (Array.isArray(value)) {
value = value.map(item => { value = value.map(item => {
let itemQuery = this.whereItemsQuery(item, options, ' AND '); let itemQuery = this.whereItemsQuery(item, options, this.OperatorMap.$and);
if ((Array.isArray(item) || _.isPlainObject(item)) && _.size(item) > 1) { if (itemQuery && itemQuery.length && (Array.isArray(item) || _.isPlainObject(item)) && _.size(item) > 1) {
itemQuery = '('+itemQuery+')'; itemQuery = '('+itemQuery+')';
} }
return itemQuery; return itemQuery;
}).filter(item => item && item.length); }).filter(item => item && item.length);
// $or: [] should return no data. value = value.length && value.join(binding);
// $not of no restriction should also return no data
if ((key === '$or' || key === '$not') && value.length === 0) {
return '0 = 1';
}
return value.length ? outerBinding + '('+value.join(binding)+')' : undefined;
} else { } else {
value = this.whereItemsQuery(value, options, binding); 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) { if ((key === '$or' || key === '$not') && !value) {
return '0 = 1'; return '0 = 1';
} }
return value ? outerBinding + '('+value+')' : undefined; return value ? outerBinding + '('+value+')' : undefined;
} },
}
if (value && (value.$or || value.$and)) {
binding = value.$or ? ' OR ' : ' AND ';
value = value.$or || value.$and;
_whereBind(binding, key, value, options) {
if (_.isPlainObject(value)) { if (_.isPlainObject(value)) {
value = _.reduce(value, (result, _value, key) => { value = _.map(value, (item, prop) => this.whereItemQuery(key, {[prop] : item}, options));
result.push(_.zipObject([key], [_value])); } else {
return result; value = value.map(item => this.whereItemQuery(key, item, options));
}, []);
} }
value = value.map(_value => this.whereItemQuery(key, _value, options)).filter(item => item && item.length); value = value.filter(item => item && item.length);
return value.length ? '('+value.join(binding)+')' : undefined; return value.length ? '('+value.join(binding)+')' : undefined;
} },
if (_.isPlainObject(value) && fieldType instanceof DataTypes.JSON && options.json !== false) { _whereJSON(key, value, options) {
const items = []; const items = [];
const traverse = (prop, item, path) => {
const where = {};
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];
}
let baseKey = this.quoteIdentifier(key); let baseKey = this.quoteIdentifier(key);
if (options.prefix) { if (options.prefix) {
if (options.prefix instanceof Utils.Literal) { if (options.prefix instanceof Utils.Literal) {
baseKey = `${this.handleSequelizeMethod(options.prefix)}.${baseKey}`; baseKey = `${this.handleSequelizeMethod(options.prefix)}.${baseKey}`;
...@@ -2125,230 +2168,198 @@ const QueryGenerator = { ...@@ -2125,230 +2168,198 @@ const QueryGenerator = {
baseKey = `${this.quoteTable(options.prefix)}.${baseKey}`; 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]);
});
baseKey = this.jsonPathExtractionQuery(baseKey, path); const result = items.join(this.OperatorMap.$and);
return items.length > 1 ? '('+result+')' : result;
const castKey = item => { },
const key = baseKey;
if (!cast) { _traverseJSON(items, baseKey, prop, item, path) {
if (typeof item === 'number') { let cast;
cast = 'double precision';
} else if (item instanceof Date) {
cast = 'timestamptz';
} else if (typeof item === 'boolean') {
cast = 'boolean';
}
}
if (cast) { if (path[path.length - 1].indexOf('::') > -1) {
return this.handleSequelizeMethod(new Utils.Cast(new Utils.Literal(key), cast)); const tmp = path[path.length - 1].split('::');
cast = tmp[1];
path[path.length - 1] = tmp[0];
} }
return key; const pathKey = this.jsonPathExtractionQuery(baseKey, path);
};
if (_.isPlainObject(item)) { if (_.isPlainObject(item)) {
_.forOwn(item, (item, prop) => { _.forOwn(item, (value, itemProp) => {
if (prop.indexOf('$') === 0) { if (itemProp.indexOf('$') === 0) {
where[prop] = item; items.push(this.whereItemQuery(this._castKey(pathKey, value, cast), {[itemProp]: value}));
const key = castKey(item); return;
items.push(this.whereItemQuery(new Utils.Literal(key), where/*, _.pick(options, 'prefix')*/));
} else {
traverse(prop, item, path.concat([prop]));
} }
this._traverseJSON(items, baseKey, itemProp, value, path.concat([itemProp]));
}); });
} else {
where.$eq = item;
const key = castKey(item);
items.push(this.whereItemQuery(new Utils.Literal(key), where/*, _.pick(options, 'prefix')*/));
}
};
_.forOwn(value, (item, prop) => {
if (prop.indexOf('$') === 0) {
const where = {};
where[prop] = item;
items.push(this.whereItemQuery(key, where, _.assign({}, options, {json: false})));
return; return;
} }
traverse(prop, item, [prop]); items.push(this.whereItemQuery(this._castKey(pathKey, item, cast), {$eq: item}));
}); },
const result = items.join(' AND '); _castKey(key, value, cast) {
return items.length > 1 ? '('+result+')' : result; 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)));
} }
// If multiple keys we combine the different logic conditions return new Utils.Literal(key);
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 ')+')';
}
// Do [] to $in/$notIn normalization _getJsonCast(value) {
if (value && (!fieldType || !(fieldType instanceof DataTypes.ARRAY))) { if (typeof value === 'number') {
if (Array.isArray(value)) { return 'double precision';
value = {
$in: value
};
} else if (value && Array.isArray(value.$not)) {
value.$notIn = value.$not;
delete value.$not;
} }
if (value instanceof Date) {
return 'timestamptz';
} }
if (typeof value === 'boolean') {
// normalize $not: non-bool|non-null to $ne return 'boolean';
if (value && typeof value.$not !== 'undefined' && [null, true, false].indexOf(value.$not) < 0) {
value.$ne = value.$not;
delete value.$not;
} }
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 (value.$notIn) comparator = 'NOT IN';
if ((value.$in || value.$notIn) instanceof Utils.Literal) {
value = (value.$in || value.$notIn).val;
} else if ((value.$in || value.$notIn).length) {
value = '('+(value.$in || value.$notIn).map(item => this.escape(item)).join(', ')+')';
} else {
if (value.$in) {
value = '(NULL)';
} else {
return '';
} }
if (comparator === undefined) {
throw new Error(`${key} and ${value} has no comperator`);
} }
} else if (value && (value.$any || value.$all)) { key = this._getSafeKey(key, prefix);
comparator = value.$any ? '= ANY' : '= ALL'; return [key, value].join(' '+comparator+' ');
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 { _getSafeKey(key, prefix) {
value = '('+this.escape(value.$any || value.$all, field)+')'; if (key instanceof Utils.SequelizeMethod) {
key = this.handleSequelizeMethod(key);
return this._prefixKey(this.handleSequelizeMethod(key), prefix);
} }
} 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 = {};
if (_.isPlainObject(value)) { return this._prefixKey(this.quoteIdentifier(key), prefix);
_.forOwn(value, (item, key) => { },
if (comparatorMap[key]) {
comparator = comparatorMap[key];
value = item;
if (_.isPlainObject(value) && value.$any) { _prefixKey(key, prefix) {
comparator += ' ANY'; if (prefix) {
escapeOptions.isList = true; if (prefix instanceof Utils.Literal) {
value = value.$any; return [this.handleSequelizeMethod(prefix), key].join('.');
} 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) { return [this.quoteTable(prefix), key].join('.');
comparator = 'IS';
} else if (comparator === '!=' && value === null) {
comparator = 'IS NOT';
} }
if (comparator.indexOf('~') !== -1) { return key;
escapeValue = false; },
}
if (this._dialect.name === 'mysql') { _whereParseSingleValueObject(key, field, prop, value, options) {
if (comparator === '~') { if (prop === '$not') {
comparator = 'REGEXP'; if (Array.isArray(value)) {
} else if (comparator === '!~') { prop = '$notIn';
comparator = 'NOT REGEXP'; } else if ([null, true, false].indexOf(value) < 0) {
prop = '$ne';
} }
} }
escapeOptions.acceptStrings = comparator.indexOf('LIKE') !== -1; let comparator = this.OperatorMap[prop] || this.OperatorMap.$eq;
escapeOptions.acceptRegExp = comparator.indexOf('~') !== -1 || comparator.indexOf('REGEXP') !== -1;
if (escapeValue) { switch (prop) {
value = this.escape(value, field, escapeOptions); case '$in':
case '$notIn':
// if ANY or ALL is used with like, add parentheses to generate correct query if (value instanceof Utils.Literal) {
if (escapeOptions.acceptStrings && ( return this._joinKeyValue(key, value.val, comparator, options.prefix);
comparator.indexOf('ANY') > comparator.indexOf('LIKE') ||
comparator.indexOf('ALL') > comparator.indexOf('LIKE')
)) {
value = '(' + value + ')';
} }
} else if (escapeOptions.acceptRegExp) {
value = '\'' + value + '\''; if (value.length) {
return this._joinKeyValue(key, `(${value.map(item => this.escape(item)).join(', ')})`, comparator, options.prefix);
} }
if (comparator === this.OperatorMap.$in) {
return this._joinKeyValue(key, '(NULL)', comparator, options.prefix);
} }
if (key) { return '';
let prefix = true; case '$any':
if (key instanceof Utils.SequelizeMethod) { case '$all':
key = this.handleSequelizeMethod(key); comparator = `${this.OperatorMap.$eq} ${comparator}`;
} else if (Utils.isColString(key)) { if (value.$values) {
key = key.substr(1, key.length - 2).split('.'); return this._joinKeyValue(key, `(VALUES ${value.$values.map(item => `(${this.escape(item)})`).join(', ')})`, comparator, options.prefix);
}
return this._joinKeyValue(key, `(${this.escape(value, field)})`, comparator, options.prefix);
case '$between':
case '$notBetween':
return this._joinKeyValue(key, `${this.escape(value[0])} AND ${this.escape(value[1])}`, comparator, options.prefix);
case '$raw':
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);
} }
return [key, value].join(' '+comparator+' '); if (value.$all) {
escapeOptions.isList = true;
return this._joinKeyValue(key, `(${this.escape(value.$all, field, escapeOptions)})`, `${comparator} ${this.OperatorMap.$all}`, options.prefix);
} }
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!