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

Commit e40f6f45 by Mickael Burguet Committed by Sushant

feat: MySQL JSON support (#7106)

1 parent 9f8ac7f9
...@@ -141,7 +141,7 @@ Sequelize.BOOLEAN // TINYINT(1) ...@@ -141,7 +141,7 @@ Sequelize.BOOLEAN // TINYINT(1)
Sequelize.ENUM('value 1', 'value 2') // An ENUM with allowed values 'value 1' and 'value 2' Sequelize.ENUM('value 1', 'value 2') // An ENUM with allowed values 'value 1' and 'value 2'
Sequelize.ARRAY(Sequelize.TEXT) // Defines an array. PostgreSQL only. Sequelize.ARRAY(Sequelize.TEXT) // Defines an array. PostgreSQL only.
Sequelize.JSON // JSON column. PostgreSQL only. Sequelize.JSON // JSON column. PostgreSQL, SQLite and MySQL only.
Sequelize.JSONB // JSONB column. PostgreSQL only. Sequelize.JSONB // JSONB column. PostgreSQL only.
Sequelize.BLOB // BLOB (bytea for PostgreSQL) Sequelize.BLOB // BLOB (bytea for PostgreSQL)
......
...@@ -453,6 +453,12 @@ HSTORE.prototype.validate = function validate(value) { ...@@ -453,6 +453,12 @@ HSTORE.prototype.validate = function validate(value) {
return true; return true;
}; };
/**
* A JSON string column. Available in postgres, sqlite and MySQL.
*
* @function JSON
* @memberof DataTypes
*/
function JSONTYPE() { function JSONTYPE() {
if (!(this instanceof JSONTYPE)) return new JSONTYPE(); if (!(this instanceof JSONTYPE)) return new JSONTYPE();
} }
......
...@@ -910,7 +910,7 @@ const QueryGenerator = { ...@@ -910,7 +910,7 @@ const QueryGenerator = {
// Users shouldn't have to worry about these args - just give them a function that takes a single arg // Users shouldn't have to worry about these args - just give them a function that takes a single arg
const simpleEscape = _.partialRight(SqlString.escape, this.options.timezone, this.dialect); const simpleEscape = _.partialRight(SqlString.escape, this.options.timezone, this.dialect);
value = field.type.stringify(value, { escape: simpleEscape, field, timezone: this.options.timezone }); value = field.type.stringify(value, { escape: simpleEscape, field, timezone: this.options.timezone, operation: options.operation });
if (field.type.escape === false) { if (field.type.escape === false) {
// The data-type already did the required escaping // The data-type already did the required escaping
...@@ -2211,7 +2211,7 @@ const QueryGenerator = { ...@@ -2211,7 +2211,7 @@ const QueryGenerator = {
if (_.isPlainObject(item)) { if (_.isPlainObject(item)) {
Utils.getOperators(item).forEach(op => { Utils.getOperators(item).forEach(op => {
const value = item[op]; const value = this._toJSONValue(item[op]);
items.push(this.whereItemQuery(this._castKey(pathKey, value, cast), {[op]: value})); items.push(this.whereItemQuery(this._castKey(pathKey, value, cast), {[op]: value}));
}); });
_.forOwn(item, (value, itemProp) => { _.forOwn(item, (value, itemProp) => {
...@@ -2221,13 +2221,18 @@ const QueryGenerator = { ...@@ -2221,13 +2221,18 @@ const QueryGenerator = {
return; return;
} }
item = this._toJSONValue(item);
items.push(this.whereItemQuery(this._castKey(pathKey, item, cast), {[Op.eq]: item})); items.push(this.whereItemQuery(this._castKey(pathKey, item, cast), {[Op.eq]: item}));
}, },
_castKey(key, value, cast) { _toJSONValue(value) {
return value;
},
_castKey(key, value, cast, json) {
cast = cast || this._getJsonCast(Array.isArray(value) ? value[0] : value); cast = cast || this._getJsonCast(Array.isArray(value) ? value[0] : value);
if (cast) { if (cast) {
return new Utils.Literal(this.handleSequelizeMethod(new Utils.Cast(new Utils.Literal(key), cast))); return new Utils.Literal(this.handleSequelizeMethod(new Utils.Cast(new Utils.Literal(key), cast, json)));
} }
return new Utils.Literal(key); return new Utils.Literal(key);
......
...@@ -34,6 +34,7 @@ module.exports = BaseTypes => { ...@@ -34,6 +34,7 @@ module.exports = BaseTypes => {
BaseTypes.REAL.types.mysql = ['DOUBLE']; BaseTypes.REAL.types.mysql = ['DOUBLE'];
BaseTypes.DOUBLE.types.mysql = ['DOUBLE']; BaseTypes.DOUBLE.types.mysql = ['DOUBLE'];
BaseTypes.GEOMETRY.types.mysql = ['GEOMETRY']; BaseTypes.GEOMETRY.types.mysql = ['GEOMETRY'];
BaseTypes.JSON.types.mysql = ['JSON'];
function BLOB(length) { function BLOB(length) {
if (!(this instanceof BLOB)) return new BLOB(length); if (!(this instanceof BLOB)) return new BLOB(length);
...@@ -177,6 +178,16 @@ module.exports = BaseTypes => { ...@@ -177,6 +178,16 @@ module.exports = BaseTypes => {
return 'ENUM(' + _.map(this.values, value => options.escape(value)).join(', ') + ')'; return 'ENUM(' + _.map(this.values, value => options.escape(value)).join(', ') + ')';
}; };
function JSONTYPE() {
if (!(this instanceof JSONTYPE)) return new JSONTYPE();
BaseTypes.JSON.apply(this, arguments);
}
inherits(JSONTYPE, BaseTypes.JSON);
JSONTYPE.prototype._stringify = function _stringify(value, options) {
return options.operation === 'where' && typeof value === 'string' ? value : JSON.stringify(value);
};
const exports = { const exports = {
ENUM, ENUM,
DATE, DATE,
...@@ -184,7 +195,8 @@ module.exports = BaseTypes => { ...@@ -184,7 +195,8 @@ module.exports = BaseTypes => {
UUID, UUID,
GEOMETRY, GEOMETRY,
DECIMAL, DECIMAL,
BLOB BLOB,
JSON: JSONTYPE
}; };
_.forIn(exports, (DataType, key) => { _.forIn(exports, (DataType, key) => {
......
...@@ -42,6 +42,7 @@ MysqlDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype. ...@@ -42,6 +42,7 @@ MysqlDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.
indexViaAlter: true, indexViaAlter: true,
NUMERIC: true, NUMERIC: true,
GEOMETRY: true, GEOMETRY: true,
JSON: true,
REGEXP: true REGEXP: true
}); });
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
const _ = require('lodash'); const _ = require('lodash');
const Utils = require('../../utils'); const Utils = require('../../utils');
const AbstractQueryGenerator = require('../abstract/query-generator'); const AbstractQueryGenerator = require('../abstract/query-generator');
const util = require('util');
const Op = require('../../operators'); const Op = require('../../operators');
const QueryGenerator = { const QueryGenerator = {
...@@ -158,6 +159,79 @@ const QueryGenerator = { ...@@ -158,6 +159,79 @@ const QueryGenerator = {
return `ALTER TABLE ${this.quoteTable(tableName)} CHANGE ${attrString.join(', ')};`; return `ALTER TABLE ${this.quoteTable(tableName)} CHANGE ${attrString.join(', ')};`;
}, },
handleSequelizeMethod(smth, tableName, factory, options, prepend) {
if (smth instanceof Utils.Json) {
// Parse nested object
if (smth.conditions) {
const conditions = _.map(this.parseConditionObject(smth.conditions), condition =>
`${this.quoteIdentifier(_.first(condition.path))}->>'\$.${_.tail(condition.path).join('.')}' = '${condition.value}'`
);
return conditions.join(' and ');
} else if (smth.path) {
let str;
// Allow specifying conditions using the sqlite json functions
if (this._checkValidJsonStatement(smth.path)) {
str = smth.path;
} else {
// Also support json dot notation
let path = smth.path;
let startWithDot = true;
// Convert .number. to [number].
path = path.replace(/\.(\d+)\./g, '[$1].');
// Convert .number$ to [number]
path = path.replace(/\.(\d+)$/, '[$1]');
path = path.split('.');
let columnName = path.shift();
const match = columnName.match(/\[\d+\]$/);
// If columnName ends with [\d+]
if (match !== null) {
path.unshift(columnName.substr(match.index));
columnName = columnName.substr(0, match.index);
startWithDot = false;
}
str = `${this.quoteIdentifier(columnName)}->>'\$${startWithDot ? '.' : ''}${path.join('.')}'`;
}
if (smth.value) {
str += util.format(' = %s', this.escape(smth.value));
}
return str;
}
} else if (smth instanceof Utils.Cast) {
if (/timestamp/i.test(smth.type)) {
smth.type = 'datetime';
} else if (smth.json && /boolean/i.test(smth.type)) {
// true or false cannot be casted as booleans within a JSON structure
smth.type = 'char';
} else if (/double precision/i.test(smth.type) || /boolean/i.test(smth.type) || /integer/i.test(smth.type)) {
smth.type = 'decimal';
} else if (/text/i.test(smth.type)) {
smth.type = 'char';
}
}
return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
},
_toJSONValue(value) {
// true/false are stored as strings in mysql
if (typeof value === 'boolean') {
return value.toString();
}
// null is stored as a string in mysql
if (value === null) {
return 'null';
}
return value;
},
upsertQuery(tableName, insertValues, updateValues, where, model, options) { upsertQuery(tableName, insertValues, updateValues, where, model, options) {
options.onDuplicate = 'UPDATE '; options.onDuplicate = 'UPDATE ';
...@@ -169,7 +243,7 @@ const QueryGenerator = { ...@@ -169,7 +243,7 @@ const QueryGenerator = {
return this.insertQuery(tableName, insertValues, model.rawAttributes, options); return this.insertQuery(tableName, insertValues, model.rawAttributes, options);
}, },
deleteQuery(tableName, where, options) { deleteQuery(tableName, where, options, model) {
options = options || {}; options = options || {};
const table = this.quoteTable(tableName); const table = this.quoteTable(tableName);
...@@ -178,7 +252,7 @@ const QueryGenerator = { ...@@ -178,7 +252,7 @@ const QueryGenerator = {
return 'TRUNCATE ' + table; return 'TRUNCATE ' + table;
} }
where = this.getWhereConditions(where); where = this.getWhereConditions(where, null, model, options);
let limit = ''; let limit = '';
if (_.isUndefined(options.limit)) { if (_.isUndefined(options.limit)) {
...@@ -312,6 +386,89 @@ const QueryGenerator = { ...@@ -312,6 +386,89 @@ const QueryGenerator = {
}, },
/** /**
* Check whether the statmement is json function or simple path
*
* @param {String} stmt The statement to validate
* @returns {Boolean} true if the given statement is json function
* @throws {Error} throw if the statement looks like json function but has invalid token
* @private
*/
_checkValidJsonStatement(stmt) {
if (!_.isString(stmt)) {
return false;
}
const jsonFunctionRegex = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]*\)/i;
const jsonOperatorRegex = /^\s*(->>?|@>|<@|\?[|&]?|\|{2}|#-)/i;
const tokenCaptureRegex = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i;
let currentIndex = 0;
let openingBrackets = 0;
let closingBrackets = 0;
let hasJsonFunction = false;
let hasInvalidToken = false;
while (currentIndex < stmt.length) {
const string = stmt.substr(currentIndex);
const functionMatches = jsonFunctionRegex.exec(string);
if (functionMatches) {
currentIndex += functionMatches[0].indexOf('(');
hasJsonFunction = true;
continue;
}
const operatorMatches = jsonOperatorRegex.exec(string);
if (operatorMatches) {
currentIndex += operatorMatches[0].length;
hasJsonFunction = true;
continue;
}
const tokenMatches = tokenCaptureRegex.exec(string);
if (tokenMatches) {
const capturedToken = tokenMatches[1];
if (capturedToken === '(') {
openingBrackets++;
} else if (capturedToken === ')') {
closingBrackets++;
} else if (capturedToken === ';') {
hasInvalidToken = true;
break;
}
currentIndex += tokenMatches[0].length;
continue;
}
break;
}
// Check invalid json statement
hasInvalidToken |= openingBrackets !== closingBrackets;
if (hasJsonFunction && hasInvalidToken) {
throw new Error('Invalid json statement: ' + stmt);
}
// return true if the statement has valid json function
return hasJsonFunction;
},
/**
* Generates an SQL query that extract JSON property of given path.
*
* @param {String} column The JSON column
* @param {String|Array<String>} [path] The path to extract (optional)
* @returns {String} The generated sql query
* @private
*/
jsonPathExtractionQuery(column, path) {
const paths = _.toPath(path);
const pathStr = `${['$'].concat(paths).join('.')}`;
const quotedColumn = this.isIdentifierQuoted(column) ? column : this.quoteIdentifier(column);
return `(${quotedColumn}->>'${pathStr}')`;
},
/**
* Generates an SQL query that returns all foreign keys of a table. * Generates an SQL query that returns all foreign keys of a table.
* *
* @param {String} tableName The name of the table. * @param {String} tableName The name of the table.
......
...@@ -132,7 +132,7 @@ const QueryGenerator = { ...@@ -132,7 +132,7 @@ const QueryGenerator = {
* @returns {Boolean} true if the given statement is json function * @returns {Boolean} true if the given statement is json function
* @throws {Error} throw if the statement looks like json function but has invalid token * @throws {Error} throw if the statement looks like json function but has invalid token
*/ */
checkValidJsonStatement(stmt) { _checkValidJsonStatement(stmt) {
if (!_.isString(stmt)) { if (!_.isString(stmt)) {
return false; return false;
} }
...@@ -220,7 +220,7 @@ const QueryGenerator = { ...@@ -220,7 +220,7 @@ const QueryGenerator = {
let str; let str;
// Allow specifying conditions using the postgres json syntax // Allow specifying conditions using the postgres json syntax
if (this.checkValidJsonStatement(smth.path)) { if (this._checkValidJsonStatement(smth.path)) {
str = smth.path; str = smth.path;
} else { } else {
// Also support json property accessors // Also support json property accessors
......
...@@ -86,7 +86,7 @@ const QueryGenerator = { ...@@ -86,7 +86,7 @@ const QueryGenerator = {
* @returns {Boolean} true if the given statement is json function * @returns {Boolean} true if the given statement is json function
* @throws {Error} throw if the statement looks like json function but has invalid token * @throws {Error} throw if the statement looks like json function but has invalid token
*/ */
checkValidJsonStatement(stmt) { _checkValidJsonStatement(stmt) {
if (!_.isString(stmt)) { if (!_.isString(stmt)) {
return false; return false;
} }
...@@ -158,40 +158,13 @@ const QueryGenerator = { ...@@ -158,40 +158,13 @@ const QueryGenerator = {
}, },
//sqlite can't cast to datetime so we need to convert date values to their ISO strings //sqlite can't cast to datetime so we need to convert date values to their ISO strings
_traverseJSON(items, baseKey, prop, item, path) { _toJSONValue(value) {
let cast; if (value instanceof Date) {
return value.toISOString();
if (path[path.length - 1].indexOf('::') > -1) { } else if (Array.isArray(value) && value[0] instanceof Date) {
const tmp = path[path.length - 1].split('::'); return value.map(val => val.toISOString());
cast = tmp[1];
path[path.length - 1] = tmp[0];
} }
return value;
const pathKey = this.jsonPathExtractionQuery(baseKey, path);
if (_.isPlainObject(item)) {
Utils.getOperators(item).forEach(op => {
let value = item[op];
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), {[op]: value}));
});
_.forOwn(item, (value, itemProp) => {
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}));
}, },
...@@ -208,7 +181,7 @@ const QueryGenerator = { ...@@ -208,7 +181,7 @@ const QueryGenerator = {
let str; let str;
// Allow specifying conditions using the sqlite json functions // Allow specifying conditions using the sqlite json functions
if (this.checkValidJsonStatement(smth.path)) { if (this._checkValidJsonStatement(smth.path)) {
str = smth.path; str = smth.path;
} else { } else {
// Also support json property accessors // Also support json property accessors
...@@ -228,6 +201,7 @@ const QueryGenerator = { ...@@ -228,6 +201,7 @@ const QueryGenerator = {
smth.type = 'datetime'; smth.type = 'datetime';
} }
} }
return AbstractQueryGenerator.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend); return AbstractQueryGenerator.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
}, },
......
...@@ -874,11 +874,11 @@ class Sequelize { ...@@ -874,11 +874,11 @@ class Sequelize {
} }
/** /**
* Creates an object representing nested where conditions for postgres's json data-type. * Creates an object representing nested where conditions for postgres/sqlite/mysql json data-type.
* @see {@link Model.findAll} * @see {@link Model#findAll}
* *
* @method json * @method json
* @param {String|Object} conditions A hash containing strings/numbers or other nested hash, a string using dot notation or a string using postgres json syntax. * @param {String|Object} conditions A hash containing strings/numbers or other nested hash, a string using dot notation or a string using postgres/sqlite/mysql json syntax.
* @param {String|Number|Boolean} [value] An optional value to compare against. Produces a string of the form "<json path> = '<value>'". * @param {String|Number|Boolean} [value] An optional value to compare against. Produces a string of the form "<json path> = '<value>'".
* @memberof Sequelize * @memberof Sequelize
* @return {Sequelize.json} * @return {Sequelize.json}
......
...@@ -497,10 +497,11 @@ class Col extends SequelizeMethod { ...@@ -497,10 +497,11 @@ class Col extends SequelizeMethod {
exports.Col = Col; exports.Col = Col;
class Cast extends SequelizeMethod { class Cast extends SequelizeMethod {
constructor(val, type) { constructor(val, type, json) {
super(); super();
this.val = val; this.val = val;
this.type = (type || '').trim(); this.type = (type || '').trim();
this.json = json || false;
} }
} }
exports.Cast = Cast; exports.Cast = Cast;
......
...@@ -34,7 +34,12 @@ describe('model', () => { ...@@ -34,7 +34,12 @@ describe('model', () => {
fields: ['id', 'username', 'document', 'emergency_contact'], fields: ['id', 'username', 'document', 'emergency_contact'],
logging: sql => { logging: sql => {
const expected = '\'{"name":"joe","phones":[1337,42]}\''; const expected = '\'{"name":"joe","phones":[1337,42]}\'';
expect(sql.indexOf(expected)).not.to.equal(-1); const expectedEscaped = '\'{\\"name\\":\\"joe\\",\\"phones\\":[1337,42]}\'';
if (sql.indexOf(expected) === -1) {
expect(sql.indexOf(expectedEscaped)).not.to.equal(-1);
} else {
expect(sql.indexOf(expected)).not.to.equal(-1);
}
} }
}); });
}); });
...@@ -235,6 +240,22 @@ describe('model', () => { ...@@ -235,6 +240,22 @@ describe('model', () => {
expect(user.username).to.equal('swen'); expect(user.username).to.equal('swen');
}); });
}); });
// JSONB Supports this, but not JSON in postgres/mysql
if (current.dialect.name === 'sqlite') {
it('should be able to find with just string', function() {
return this.User.create({
username: 'swen123',
emergency_contact: 'Unknown',
}).then(() => {
return this.User.find({where: {
emergency_contact: 'Unknown',
}});
}).then(user => {
expect(user.username).to.equal('swen123');
});
});
}
}); });
} }
}); });
...@@ -251,9 +251,9 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -251,9 +251,9 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}); });
it('should be possible to query dates with array operators', function() { it('should be possible to query dates with array operators', function() {
const now = moment().toDate(); const now = moment().milliseconds(0).toDate();
const before = moment().subtract(1, 'day').toDate(); const before = moment().milliseconds(0).subtract(1, 'day').toDate();
const after = moment().add(1, 'day').toDate(); const after = moment().milliseconds(0).add(1, 'day').toDate();
return Promise.join( return Promise.join(
this.Event.create({ this.Event.create({
json: { json: {
......
...@@ -102,7 +102,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -102,7 +102,7 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}); });
}); });
if (current.dialect.supports.JSON) { if (current.dialect.supports.JSONB) {
describe('JSONB', () => { describe('JSONB', () => {
before(function() { before(function() {
this.Model = this.sequelize.define('Model', { this.Model = this.sequelize.define('Model', {
......
...@@ -9,14 +9,14 @@ const Support = require(__dirname + '/../support'), ...@@ -9,14 +9,14 @@ const Support = require(__dirname + '/../support'),
sql = current.dialect.QueryGenerator; sql = current.dialect.QueryGenerator;
// Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation // Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation
if (current.dialect.supports.JSON) { if (current.dialect.supports.JSON) {
suite(Support.getTestDialectTeaser('SQL'), () => { suite(Support.getTestDialectTeaser('SQL'), () => {
suite('JSON', () => { suite('JSON', () => {
suite('escape', () => { suite('escape', () => {
test('plain string', () => { test('plain string', () => {
expectsql(sql.escape('string', { type: new DataTypes.JSON() }), { expectsql(sql.escape('string', { type: new DataTypes.JSON() }), {
default: '\'"string"\'' default: '\'"string"\'',
mysql: '\'\\"string\\"\''
}); });
}); });
...@@ -46,7 +46,8 @@ if (current.dialect.supports.JSON) { ...@@ -46,7 +46,8 @@ if (current.dialect.supports.JSON) {
test('nested object', () => { test('nested object', () => {
expectsql(sql.escape({ some: 'nested', more: { nested: true }, answer: 42 }, { type: new DataTypes.JSON() }), { expectsql(sql.escape({ some: 'nested', more: { nested: true }, answer: 42 }, { type: new DataTypes.JSON() }), {
default: '\'{"some":"nested","more":{"nested":true},"answer":42}\'' default: '\'{"some":"nested","more":{"nested":true},"answer":42}\'',
mysql: '\'{\\"some\\":\\"nested\\",\\"more\\":{\\"nested\\":true},\\"answer\\":42}\''
}); });
}); });
...@@ -79,35 +80,40 @@ if (current.dialect.supports.JSON) { ...@@ -79,35 +80,40 @@ if (current.dialect.supports.JSON) {
test('condition object', () => { test('condition object', () => {
expectsql(sql.whereItemQuery(undefined, Sequelize.json({ id: 1 })), { expectsql(sql.whereItemQuery(undefined, Sequelize.json({ id: 1 })), {
postgres: '("id"#>>\'{}\') = \'1\'', postgres: '("id"#>>\'{}\') = \'1\'',
sqlite: "json_extract(`id`, '$') = '1'" sqlite: "json_extract(`id`, '$') = '1'",
mysql: "`id`->>'$.' = '1'"
}); });
}); });
test('nested condition object', () => { test('nested condition object', () => {
expectsql(sql.whereItemQuery(undefined, Sequelize.json({ profile: { id: 1 } })), { expectsql(sql.whereItemQuery(undefined, Sequelize.json({ profile: { id: 1 } })), {
postgres: '("profile"#>>\'{id}\') = \'1\'', postgres: '("profile"#>>\'{id}\') = \'1\'',
sqlite: "json_extract(`profile`, '$.id') = '1'" sqlite: "json_extract(`profile`, '$.id') = '1'",
mysql: "`profile`->>'$.id' = '1'"
}); });
}); });
test('multiple condition object', () => { test('multiple condition object', () => {
expectsql(sql.whereItemQuery(undefined, Sequelize.json({ property: { value: 1 }, another: { value: 'string' } })), { expectsql(sql.whereItemQuery(undefined, Sequelize.json({ property: { value: 1 }, another: { value: 'string' } })), {
postgres: '("property"#>>\'{value}\') = \'1\' AND ("another"#>>\'{value}\') = \'string\'', postgres: '("property"#>>\'{value}\') = \'1\' AND ("another"#>>\'{value}\') = \'string\'',
sqlite: "json_extract(`property`, '$.value') = '1' AND json_extract(`another`, '$.value') = 'string'" sqlite: "json_extract(`property`, '$.value') = '1' AND json_extract(`another`, '$.value') = 'string'",
mysql: "`property`->>'$.value' = '1' and `another`->>'$.value' = 'string'"
}); });
}); });
test('dot notaion', () => { test('dot notation', () => {
expectsql(sql.whereItemQuery(Sequelize.json('profile.id'), '1'), { expectsql(sql.whereItemQuery(Sequelize.json('profile.id'), '1'), {
postgres: '("profile"#>>\'{id}\') = \'1\'', postgres: '("profile"#>>\'{id}\') = \'1\'',
sqlite: "json_extract(`profile`, '$.id') = '1'" sqlite: "json_extract(`profile`, '$.id') = '1'",
mysql: "`profile`->>'$.id' = '1'"
}); });
}); });
test('column named "json"', () => { test('column named "json"', () => {
expectsql(sql.whereItemQuery(Sequelize.json('json'), '{}'), { expectsql(sql.whereItemQuery(Sequelize.json('json'), '{}'), {
postgres: '("json"#>>\'{}\') = \'{}\'', postgres: '("json"#>>\'{}\') = \'{}\'',
sqlite: "json_extract(`json`, '$') = '{}'" sqlite: "json_extract(`json`, '$') = '{}'",
mysql: "`json`->>'$.' = '{}'"
}); });
}); });
}); });
......
...@@ -769,7 +769,16 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -769,7 +769,16 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
test('sequelize.json("profile.id"), sequelize.cast(2, \'text\')")', function() { test('sequelize.json("profile.id"), sequelize.cast(2, \'text\')")', function() {
expectsql(sql.whereItemQuery(undefined, this.sequelize.json('profile.id', this.sequelize.cast('12346-78912', 'text'))), { expectsql(sql.whereItemQuery(undefined, this.sequelize.json('profile.id', this.sequelize.cast('12346-78912', 'text'))), {
postgres: "(\"profile\"#>>'{id}') = CAST('12346-78912' AS TEXT)", postgres: "(\"profile\"#>>'{id}') = CAST('12346-78912' AS TEXT)",
sqlite: "json_extract(`profile`, '$.id') = CAST('12346-78912' AS TEXT)" sqlite: "json_extract(`profile`, '$.id') = CAST('12346-78912' AS TEXT)",
mysql: "`profile`->>'$.id' = CAST('12346-78912' AS CHAR)"
});
});
test('sequelize.json({profile: {id: "12346-78912", name: "test"}})', function () {
expectsql(sql.whereItemQuery(undefined, this.sequelize.json({profile: {id: '12346-78912', name: 'test'}})), {
postgres: "(\"profile\"#>>'{id}') = '12346-78912' AND (\"profile\"#>>'{name}') = 'test'",
sqlite: "json_extract(`profile`, '$.id') = '12346-78912' AND json_extract(`profile`, '$.name') = 'test'",
mysql: "`profile`->>'$.id' = '12346-78912' and `profile`->>'$.name' = 'test'"
}); });
}); });
...@@ -783,6 +792,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -783,6 +792,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
}, },
prefix: 'User' prefix: 'User'
}, { }, {
mysql: "(`User`.`data`->>'$.nested.attribute') = 'value'",
postgres: "(\"User\".\"data\"#>>'{nested,attribute}') = 'value'", postgres: "(\"User\".\"data\"#>>'{nested,attribute}') = 'value'",
sqlite: "json_extract(`User`.`data`, '$.nested.attribute') = 'value'" sqlite: "json_extract(`User`.`data`, '$.nested.attribute') = 'value'"
}); });
...@@ -796,6 +806,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -796,6 +806,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
type: new DataTypes.JSONB() type: new DataTypes.JSONB()
} }
}, { }, {
mysql: "CAST((`data`->>'$.nested') AS DECIMAL) IN (1, 2)",
postgres: "CAST((\"data\"#>>'{nested}') AS DOUBLE PRECISION) IN (1, 2)", postgres: "CAST((\"data\"#>>'{nested}') AS DOUBLE PRECISION) IN (1, 2)",
sqlite: "CAST(json_extract(`data`, '$.nested') AS DOUBLE PRECISION) IN (1, 2)" sqlite: "CAST(json_extract(`data`, '$.nested') AS DOUBLE PRECISION) IN (1, 2)"
}); });
...@@ -809,6 +820,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -809,6 +820,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
type: new DataTypes.JSONB() type: new DataTypes.JSONB()
} }
}, { }, {
mysql: "CAST((`data`->>'$.nested') AS DECIMAL) BETWEEN 1 AND 2",
postgres: "CAST((\"data\"#>>'{nested}') AS DOUBLE PRECISION) BETWEEN 1 AND 2", postgres: "CAST((\"data\"#>>'{nested}') AS DOUBLE PRECISION) BETWEEN 1 AND 2",
sqlite: "CAST(json_extract(`data`, '$.nested') AS DOUBLE PRECISION) BETWEEN 1 AND 2" sqlite: "CAST(json_extract(`data`, '$.nested') AS DOUBLE PRECISION) BETWEEN 1 AND 2"
}); });
...@@ -824,8 +836,9 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -824,8 +836,9 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
field: { field: {
type: new DataTypes.JSONB() type: new DataTypes.JSONB()
}, },
prefix: 'User' prefix: current.literal(sql.quoteTable.call(current.dialect.QueryGenerator, {tableName: 'User'}))
}, { }, {
mysql: "((`User`.`data`->>'$.nested.attribute') = 'value' AND (`User`.`data`->>'$.nested.prop') != 'None')",
postgres: "((\"User\".\"data\"#>>'{nested,attribute}') = 'value' AND (\"User\".\"data\"#>>'{nested,prop}') != 'None')", postgres: "((\"User\".\"data\"#>>'{nested,attribute}') = 'value' AND (\"User\".\"data\"#>>'{nested,prop}') != 'None')",
sqlite: "(json_extract(`User`.`data`, '$.nested.attribute') = 'value' AND json_extract(`User`.`data`, '$.nested.prop') != 'None')" sqlite: "(json_extract(`User`.`data`, '$.nested.attribute') = 'value' AND json_extract(`User`.`data`, '$.nested.prop') != 'None')"
}); });
...@@ -843,6 +856,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -843,6 +856,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
}, },
prefix: 'User' prefix: 'User'
}, { }, {
mysql: "((`User`.`data`->>'$.name.last') = 'Simpson' AND (`User`.`data`->>'$.employment') != 'None')",
postgres: "((\"User\".\"data\"#>>'{name,last}') = 'Simpson' AND (\"User\".\"data\"#>>'{employment}') != 'None')", postgres: "((\"User\".\"data\"#>>'{name,last}') = 'Simpson' AND (\"User\".\"data\"#>>'{employment}') != 'None')",
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')"
}); });
...@@ -855,6 +869,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -855,6 +869,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
type: new DataTypes.JSONB() type: new DataTypes.JSONB()
} }
}, { }, {
mysql: "(CAST((`data`->>'$.price') AS DECIMAL) = 5 AND (`data`->>'$.name') = 'Product')",
postgres: "(CAST((\"data\"#>>'{price}') AS DOUBLE PRECISION) = 5 AND (\"data\"#>>'{name}') = 'Product')", 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')" sqlite: "(CAST(json_extract(`data`, '$.price') AS DOUBLE PRECISION) = 5 AND json_extract(`data`, '$.name') = 'Product')"
}); });
...@@ -868,6 +883,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -868,6 +883,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
} }
} }
}, { }, {
mysql: "(`data`->>'$.nested.attribute') = 'value'",
postgres: "(\"data\"#>>'{nested,attribute}') = 'value'", postgres: "(\"data\"#>>'{nested,attribute}') = 'value'",
sqlite: "json_extract(`data`, '$.nested.attribute') = 'value'" sqlite: "json_extract(`data`, '$.nested.attribute') = 'value'"
}); });
...@@ -881,6 +897,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -881,6 +897,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
} }
} }
}, { }, {
mysql: "CAST((`data`->>'$.nested.attribute') AS DECIMAL) = 4",
postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) = 4", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) = 4",
sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DOUBLE PRECISION) = 4" sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DOUBLE PRECISION) = 4"
}); });
...@@ -896,6 +913,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -896,6 +913,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
} }
} }
}, { }, {
mysql: "CAST((`data`->>'$.nested.attribute') AS DECIMAL) IN (3, 7)",
postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) 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)" sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DOUBLE PRECISION) IN (3, 7)"
}); });
...@@ -911,6 +929,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -911,6 +929,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
type: new DataTypes.JSONB() type: new DataTypes.JSONB()
} }
}, { }, {
mysql: "CAST((`data`->>'$.nested.attribute') AS DECIMAL) > 2",
postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) > 2", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) > 2",
sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DOUBLE PRECISION) > 2" sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DOUBLE PRECISION) > 2"
}); });
...@@ -926,6 +945,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -926,6 +945,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
type: new DataTypes.JSONB() type: new DataTypes.JSONB()
} }
}, { }, {
mysql: "CAST((`data`->>'$.nested.attribute') AS DECIMAL) > 2",
postgres: "CAST((\"data\"#>>'{nested,attribute}') AS INTEGER) > 2", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS INTEGER) > 2",
sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS INTEGER) > 2" sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS INTEGER) > 2"
}); });
...@@ -942,6 +962,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -942,6 +962,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
type: new DataTypes.JSONB() type: new DataTypes.JSONB()
} }
}, { }, {
mysql: "CAST((`data`->>'$.nested.attribute') AS DATETIME) > "+sql.escape(dt),
postgres: "CAST((\"data\"#>>'{nested,attribute}') AS TIMESTAMPTZ) > "+sql.escape(dt), postgres: "CAST((\"data\"#>>'{nested,attribute}') AS TIMESTAMPTZ) > "+sql.escape(dt),
sqlite: "json_extract(`data`, '$.nested.attribute') > " + sql.escape(dt.toISOString()) sqlite: "json_extract(`data`, '$.nested.attribute') > " + sql.escape(dt.toISOString())
}); });
...@@ -955,6 +976,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -955,6 +976,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
type: new DataTypes.JSONB() type: new DataTypes.JSONB()
} }
}, { }, {
mysql: "(`data`->>'$.nested.attribute') = 'true'",
postgres: "CAST((\"data\"#>>'{nested,attribute}') AS BOOLEAN) = true", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS BOOLEAN) = true",
sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS BOOLEAN) = 1" sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS BOOLEAN) = 1"
}); });
...@@ -970,6 +992,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -970,6 +992,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
} }
} }
}, { }, {
mysql: "(`meta_data`->>'$.nested.attribute') = 'value'",
postgres: "(\"meta_data\"#>>'{nested,attribute}') = 'value'", postgres: "(\"meta_data\"#>>'{nested,attribute}') = 'value'",
sqlite: "json_extract(`meta_data`, '$.nested.attribute') = 'value'" sqlite: "json_extract(`meta_data`, '$.nested.attribute') = 'value'"
}); });
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!