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

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 = {
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) {
if (options.model.rawAttributes[key.split('.')[0]] && options.model.rawAttributes[key.split('.')[0]].type instanceof DataTypes.JSON) {
field = options.model.rawAttributes[key.split('.')[0]];
fieldType = field.type;
const tmp = value;
value = {};
Dottie.set(value, key.split('.').slice(1), tmp);
key = field.field || key.split('.')[0];
}
}
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'
},
const comparatorMap = {
OperatorMap: {
$eq: '=',
$ne: '!=',
$gte: '>=',
......@@ -1992,132 +1997,170 @@ const QueryGenerator = {
$strictLeft: '<<',
$strictRight: '>>',
$noExtendRight: '&<',
$noExtendLeft: '&>'
};
// Maintain BC
const aliasMap = {
'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'
};
$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
},
key = aliasMap[key] || key;
if (_.isPlainObject(value)) {
_.forOwn(value, (item, key) => {
if (aliasMap[key]) {
value[aliasMap[key]] = item;
delete value[key];
whereItemQuery(key, value, options) {
options = options || {};
if (key && typeof key === 'string' && key.indexOf('.') !== -1 && options.model) {
const keyParts = key.split('.');
if (options.model.rawAttributes[keyParts[0]] && options.model.rawAttributes[keyParts[0]].type instanceof DataTypes.JSON) {
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 (typeof value === 'string') {
return value;
}
if (_.isPlainObject(value) && _.size(value) === 1) {
key = Object.keys(value)[0];
value = _.values(value)[0];
if (isPlainObject && valueKeys.length === 1) {
return this.whereItemQuery(valueKeys[0], value[valueKeys[0]], options);
}
}
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);
}
// 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)) {
key = '$and';
} else {
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') {
binding = key === '$or' ?' OR ' : ' AND ';
outerBinding = '';
if (key === '$not') outerBinding = 'NOT ';
return this._whereGroupBind(key, value, options);
}
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)) {
value = value.map(item => {
let itemQuery = this.whereItemsQuery(item, options, ' AND ');
if ((Array.isArray(item) || _.isPlainObject(item)) && _.size(item) > 1) {
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);
// $or: [] should return no data.
// $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;
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';
}
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)) {
value = _.reduce(value, (result, _value, key) => {
result.push(_.zipObject([key], [_value]));
return result;
}, []);
value = _.map(value, (item, prop) => this.whereItemQuery(key, {[prop] : item}, options));
} else {
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;
}
},
if (_.isPlainObject(value) && fieldType instanceof DataTypes.JSON && options.json !== false) {
_whereJSON(key, value, options) {
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);
if (options.prefix) {
if (options.prefix instanceof Utils.Literal) {
baseKey = `${this.handleSequelizeMethod(options.prefix)}.${baseKey}`;
......@@ -2125,230 +2168,198 @@ const QueryGenerator = {
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 castKey = item => {
const key = baseKey;
const result = items.join(this.OperatorMap.$and);
return items.length > 1 ? '('+result+')' : result;
},
if (!cast) {
if (typeof item === 'number') {
cast = 'double precision';
} else if (item instanceof Date) {
cast = 'timestamptz';
} else if (typeof item === 'boolean') {
cast = 'boolean';
}
}
_traverseJSON(items, baseKey, prop, item, path) {
let cast;
if (cast) {
return this.handleSequelizeMethod(new Utils.Cast(new Utils.Literal(key), cast));
if (path[path.length - 1].indexOf('::') > -1) {
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)) {
_.forOwn(item, (item, prop) => {
if (prop.indexOf('$') === 0) {
where[prop] = item;
const key = castKey(item);
items.push(this.whereItemQuery(new Utils.Literal(key), where/*, _.pick(options, 'prefix')*/));
} else {
traverse(prop, item, path.concat([prop]));
_.forOwn(item, (value, itemProp) => {
if (itemProp.indexOf('$') === 0) {
items.push(this.whereItemQuery(this._castKey(pathKey, value, cast), {[itemProp]: value}));
return;
}
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;
}
traverse(prop, item, [prop]);
});
items.push(this.whereItemQuery(this._castKey(pathKey, item, cast), {$eq: item}));
},
const result = items.join(' AND ');
return items.length > 1 ? '('+result+')' : result;
_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)));
}
// If multiple keys we combine the different logic conditions
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 ')+')';
}
return new Utils.Literal(key);
},
// Do [] to $in/$notIn normalization
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;
_getJsonCast(value) {
if (typeof value === 'number') {
return 'double precision';
}
if (value instanceof Date) {
return 'timestamptz';
}
// normalize $not: non-bool|non-null to $ne
if (value && typeof value.$not !== 'undefined' && [null, true, false].indexOf(value.$not) < 0) {
value.$ne = value.$not;
delete value.$not;
if (typeof value === 'boolean') {
return 'boolean';
}
return;
},
// Setup keys and comparators
if (Array.isArray(value) && fieldType instanceof DataTypes.ARRAY) {
value = this.escape(value, field);
} 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 '';
_joinKeyValue(key, value, comparator, prefix) {
if (!key) {
return value;
}
if (comparator === undefined) {
throw new Error(`${key} and ${value} has no comperator`);
}
} 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)+')';
key = this._getSafeKey(key, prefix);
return [key, value].join(' '+comparator+' ');
},
_getSafeKey(key, prefix) {
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 ');
} else if (value && value.$raw) {
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 (Utils.isColString(key)) {
key = key.substr(1, key.length - 2).split('.');
if (value.length > 2) {
value = [
if (key.length > 2) {
key = [
// join the tables by -> to match out internal namings
value.slice(0, -1).join('->'),
value[value.length - 1]
key.slice(0, -1).join('->'),
key[key.length - 1]
];
}
value = value.map(identifier => this.quoteIdentifier(identifier)).join('.');
} else {
let escapeValue = true;
const escapeOptions = {};
return key.map(identifier => this.quoteIdentifier(identifier)).join('.');
}
if (_.isPlainObject(value)) {
_.forOwn(value, (item, key) => {
if (comparatorMap[key]) {
comparator = comparatorMap[key];
value = item;
return this._prefixKey(this.quoteIdentifier(key), prefix);
},
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);
}
}
});
_prefixKey(key, prefix) {
if (prefix) {
if (prefix instanceof Utils.Literal) {
return [this.handleSequelizeMethod(prefix), key].join('.');
}
if (comparator === '=' && value === null) {
comparator = 'IS';
} else if (comparator === '!=' && value === null) {
comparator = 'IS NOT';
return [this.quoteTable(prefix), key].join('.');
}
if (comparator.indexOf('~') !== -1) {
escapeValue = false;
}
return key;
},
if (this._dialect.name === 'mysql') {
if (comparator === '~') {
comparator = 'REGEXP';
} else if (comparator === '!~') {
comparator = 'NOT REGEXP';
_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';
}
}
escapeOptions.acceptStrings = comparator.indexOf('LIKE') !== -1;
escapeOptions.acceptRegExp = comparator.indexOf('~') !== -1 || comparator.indexOf('REGEXP') !== -1;
let comparator = this.OperatorMap[prop] || this.OperatorMap.$eq;
if (escapeValue) {
value = this.escape(value, field, escapeOptions);
// if ANY or ALL is used with like, add parentheses to generate correct query
if (escapeOptions.acceptStrings && (
comparator.indexOf('ANY') > comparator.indexOf('LIKE') ||
comparator.indexOf('ALL') > comparator.indexOf('LIKE')
)) {
value = '(' + value + ')';
switch (prop) {
case '$in':
case '$notIn':
if (value instanceof Utils.Literal) {
return this._joinKeyValue(key, value.val, comparator, options.prefix);
}
} 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) {
let prefix = true;
if (key instanceof Utils.SequelizeMethod) {
key = this.handleSequelizeMethod(key);
} else if (Utils.isColString(key)) {
key = key.substr(1, key.length - 2).split('.');
return '';
case '$any':
case '$all':
comparator = `${this.OperatorMap.$eq} ${comparator}`;
if (value.$values) {
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) {
key = [
if (value.length > 2) {
value = [
// join the tables by -> to match out internal namings
key.slice(0, -1).join('->'),
key[key.length - 1]
value.slice(0, -1).join('->'),
value[value.length - 1]
];
}
key = key.map(identifier => this.quoteIdentifier(identifier)).join('.');
prefix = false;
} else {
key = this.quoteIdentifier(key);
return this._joinKeyValue(key, value.map(identifier => this.quoteIdentifier(identifier)).join('.'), comparator, options.prefix);
}
if (options.prefix && prefix) {
if (options.prefix instanceof Utils.Literal) {
key = [this.handleSequelizeMethod(options.prefix), key].join('.');
} else {
key = [this.quoteTable(options.prefix), key].join('.');
const escapeOptions = {
acceptStrings: comparator.indexOf(this.OperatorMap.$like) !== -1
};
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 = {
__proto__: AbstractQueryGenerator,
dialect: 'mysql',
OperatorMap: Object.assign({}, AbstractQueryGenerator.OperatorMap, {
$regexp: 'REGEXP',
$notRegexp: 'NOT REGEXP'
}),
createSchema() {
return 'SHOW TABLES';
},
......
......@@ -157,6 +157,44 @@ const QueryGenerator = {
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) {
if (smth instanceof Utils.Json) {
// Parse nested object
......
......@@ -3,6 +3,7 @@
const chai = require('chai'),
Sequelize = require('../../../index'),
Promise = Sequelize.Promise,
moment = require('moment'),
expect = chai.expect,
Support = require(__dirname + '/../support'),
DataTypes = require(__dirname + '/../../../lib/data-types'),
......@@ -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() {
return Promise.join(
this.Event.create({
......
......@@ -789,6 +789,32 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
testsql('data', {
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',
prop: {
$ne: 'None'
......@@ -821,6 +847,18 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
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', {
model: {
rawAttributes: {
......@@ -858,8 +896,8 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
}
}
}, {
postgres: "(\"data\"#>>'{nested,attribute}') IN (3, 7)",
sqlite: "json_extract(`data`, '$.nested.attribute') IN (3, 7)"
postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) IN (3, 7)",
sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DOUBLE PRECISION) IN (3, 7)"
});
testsql('data', {
......@@ -905,7 +943,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
}
}, {
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', {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!