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

Commit cd2e38e4 by Ali Taheri Moghaddar Committed by Jan Aagaard Meier

Tidy up range type (#5990)

* Make range bound defaults adhere to postgres

* Explicitly support RANGE for postgres

* Support serializing casted range subtype values

* Support all range operators

* Document range types and operators
1 parent e7f5544b
# Future
- [CHANGED] Range type bounds now default to [postgres default](https://www.postgresql.org/docs/9.5/static/rangetypes.html#RANGETYPES-CONSTRUCT) `[)` (inclusive, exclusive) [#5990](https://github.com/sequelize/sequelize/issues/5990)
- [ADDED] Support for range operators [#5990](https://github.com/sequelize/sequelize/issues/5990)
## BC breaks:
- Range type bounds now default to [postgres default](https://www.postgresql.org/docs/9.5/static/rangetypes.html#RANGETYPES-CONSTRUCT) `[)` (inclusive, exclusive), previously was `()` (exclusive, exclusive)
# 4.0.0-0 # 4.0.0-0
- [FIXED] Pass ResourceLock instead of raw connection in MSSQL disconnect handling - [FIXED] Pass ResourceLock instead of raw connection in MSSQL disconnect handling
- [CHANGED] Remove `hookValidate` in favor of `validate` with `hooks: true | false`. - [CHANGED] Remove `hookValidate` in favor of `validate` with `hooks: true | false`.
......
...@@ -171,6 +171,71 @@ sequelize.define('model', { ...@@ -171,6 +171,71 @@ sequelize.define('model', {
}) })
``` ```
### Range types
Since range types have extra information for their bound inclusion/exclusion it's not
very straightforward to just use a touple to represent them in javascript.
When supplying ranges as values you can choose from the following APIs:
```js
// defaults to '["2016-01-01 00:00:00+00:00", "2016-02-01 00:00:00+00:00")'
// inclusive lower bound, exclusive upper bound
Timeline.create({ range: [new Date(Date.UTC(2016, 0, 1)), new Date(Date.UTC(2016, 1, 1))] });
// control inclusion
const range = [new Date(Date.UTC(2016, 0, 1)), new Date(Date.UTC(2016, 1, 1))];
range.inclusive = false; // '()'
range.inclusive = [false, true]; // '(]'
range.inclusive = true; // '[]'
range.inclusive = [true, false]; // '[)'
// or as a single expression
const range = [
{ value: new Date(Date.UTC(2016, 0, 1)), inclusive: false },
{ value: new Date(Date.UTC(2016, 1, 1)), inclusive: true },
];
// '("2016-01-01 00:00:00+00:00", "2016-02-01 00:00:00+00:00"]'
// composite form
const range = [
{ value: new Date(Date.UTC(2016, 0, 1)), inclusive: false },
new Date(Date.UTC(2016, 1, 1)),
];
// '("2016-01-01 00:00:00+00:00", "2016-02-01 00:00:00+00:00")'
Timeline.create({ range });
```
However, please note that whenever you get back a value that is range you will
receive:
```js
// stored value: ("2016-01-01 00:00:00+00:00", "2016-02-01 00:00:00+00:00"]
range // [Date, Date]
range.inclusive // [false, true]
```
Make sure you turn that into a serializable format before serialization since array
extra properties will not be serialized.
#### Special Cases
```js
// empty range:
Timeline.create({ range: [] }); // range = 'empty'
// Unbounded range:
Timeline.create({ range: [null, null] }); // range = '[,)'
// range = '[,"2016-01-01 00:00:00+00:00")'
Timeline.create({ range: [null, new Date(Date.UTC(2016, 0, 1))] });
// Infinite range:
// range = '[-infinity,"2016-01-01 00:00:00+00:00")'
Timeline.create({ range: [-Infinity, new Date(Date.UTC(2016, 0, 1))] });
```
## Deferrable ## Deferrable
When you specify a foreign key column it is optionally possible to declare the deferrable When you specify a foreign key column it is optionally possible to declare the deferrable
......
...@@ -142,6 +142,28 @@ $any: [2,3] // ANY ARRAY[2, 3]::INTEGER (PG only) ...@@ -142,6 +142,28 @@ $any: [2,3] // ANY ARRAY[2, 3]::INTEGER (PG only)
$col: 'user.organization_id' // = "user"."organization_id", with dialect specific column identifiers, PG in this example $col: 'user.organization_id' // = "user"."organization_id", with dialect specific column identifiers, PG in this example
``` ```
### Range Operators
Range types can be queried with all supported operators.
Keep in mind, the provided range value can
[define the bound inclusion/exclusion](models-definition/#range-types)
as well.
```js
// All the above equlity and inequality operators plus the following:
$contains: 2 // @> '2'::integer (PG range contains element operator)
$contains: [1, 2] // @> [1, 2) (PG range contains range operator)
$contained: [1, 2] // <@ [1, 2) (PG range is contained by operator)
$overlap: [1, 2] // && [1, 2) (PG range overlap (have points in common) operator)
$adjacent: [1, 2] // -|- [1, 2) (PG range is adjacent to operator)
$strictLeft: [1, 2] // << [1, 2) (PG range strictly left of operator)
$strictRight: [1, 2] // >> [1, 2) (PG range strictly right of operator)
$noExtendRight: [1, 2] // &< [1, 2) (PG range does not extend to the right of operator)
$noExtendLeft: [1, 2] // &> [1, 2) (PG range does not extend to the left of operator)
```
### Combinations ### Combinations
```js ```js
{ {
......
...@@ -643,10 +643,22 @@ var pgRangeSubtypes = { ...@@ -643,10 +643,22 @@ var pgRangeSubtypes = {
datenotz: 'tsrange' datenotz: 'tsrange'
}; };
const pgRangeCastTypes = {
integer: 'integer',
bigint: 'bigint',
decimal: 'numeric',
dateonly: 'date',
date: 'timestamptz',
datenotz: 'timestamp'
};
RANGE.prototype.key = RANGE.key = 'RANGE'; RANGE.prototype.key = RANGE.key = 'RANGE';
RANGE.prototype.toSql = function() { RANGE.prototype.toSql = function() {
return pgRangeSubtypes[this._subtype.toLowerCase()]; return pgRangeSubtypes[this._subtype.toLowerCase()];
}; };
RANGE.prototype.toCastType = function() {
return pgRangeCastTypes[this._subtype.toLowerCase()];
};
RANGE.prototype.validate = function(value) { RANGE.prototype.validate = function(value) {
if (_.isPlainObject(value) && value.inclusive) { if (_.isPlainObject(value) && value.inclusive) {
value = value.inclusive; value = value.inclusive;
......
...@@ -1940,7 +1940,12 @@ var QueryGenerator = { ...@@ -1940,7 +1940,12 @@ var QueryGenerator = {
$notBetween: 'NOT BETWEEN', $notBetween: 'NOT BETWEEN',
$overlap: '&&', $overlap: '&&',
$contains: '@>', $contains: '@>',
$contained: '<@' $contained: '<@',
$adjacent: '-|-',
$strictLeft: '<<',
$strictRight: '>>',
$noExtendRight: '&<',
$noExtendLeft: '&>'
}; };
// Maintain BC // Maintain BC
......
...@@ -361,6 +361,10 @@ module.exports = function (BaseTypes) { ...@@ -361,6 +361,10 @@ module.exports = function (BaseTypes) {
RANGE.prototype.escape = false; RANGE.prototype.escape = false;
RANGE.prototype.$stringify = function (values, options) { RANGE.prototype.$stringify = function (values, options) {
if (!Array.isArray(values)) {
return "'" + this.options.subtype.stringify(values, options) + "'::" +
this.toCastType();
}
var valuesStringified = values.map(function (value) { var valuesStringified = values.map(function (value) {
if (this.options.subtype.stringify) { if (this.options.subtype.stringify) {
return this.options.subtype.stringify(value, options); return this.options.subtype.stringify(value, options);
......
...@@ -41,6 +41,7 @@ PostgresDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supp ...@@ -41,6 +41,7 @@ PostgresDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supp
}, },
NUMERIC: true, NUMERIC: true,
ARRAY: true, ARRAY: true,
RANGE: true,
GEOMETRY: true, GEOMETRY: true,
GEOGRAPHY: true, GEOGRAPHY: true,
JSON: true, JSON: true,
......
...@@ -32,10 +32,11 @@ function stringify (data) { ...@@ -32,10 +32,11 @@ function stringify (data) {
if (data.length !== 2) throw new Error('range array length must be 0 (empty) or 2 (lower and upper bounds)'); if (data.length !== 2) throw new Error('range array length must be 0 (empty) or 2 (lower and upper bounds)');
if (data.hasOwnProperty('inclusive')) { if (data.hasOwnProperty('inclusive')) {
if (!data.inclusive) data.inclusive = [false, false]; if (data.inclusive === false) data.inclusive = [false, false];
else if (!data.inclusive) data.inclusive = [true, false];
else if (data.inclusive === true) data.inclusive = [true, true]; else if (data.inclusive === true) data.inclusive = [true, true];
} else { } else {
data.inclusive = [false, false]; data.inclusive = [true, false];
} }
_.each(data, function (value, index) { _.each(data, function (value, index) {
......
...@@ -647,12 +647,12 @@ if (dialect.match(/^postgres/)) { ...@@ -647,12 +647,12 @@ if (dialect.match(/^postgres/)) {
expect(newUser.acceptable_marks.length).to.equal(2); expect(newUser.acceptable_marks.length).to.equal(2);
expect(newUser.acceptable_marks[0]).to.equal('0.65'); // lower bound expect(newUser.acceptable_marks[0]).to.equal('0.65'); // lower bound
expect(newUser.acceptable_marks[1]).to.equal('1'); // upper bound expect(newUser.acceptable_marks[1]).to.equal('1'); // upper bound
expect(newUser.acceptable_marks.inclusive).to.deep.equal([false, false]); // not inclusive expect(newUser.acceptable_marks.inclusive).to.deep.equal([true, false]); // inclusive, exclusive
expect(newUser.course_period[0] instanceof Date).to.be.ok; // lower bound expect(newUser.course_period[0] instanceof Date).to.be.ok; // lower bound
expect(newUser.course_period[1] instanceof Date).to.be.ok; // upper bound expect(newUser.course_period[1] instanceof Date).to.be.ok; // upper bound
expect(newUser.course_period[0]).to.equalTime(period[0]); // lower bound expect(newUser.course_period[0]).to.equalTime(period[0]); // lower bound
expect(newUser.course_period[1]).to.equalTime(period[1]); // upper bound expect(newUser.course_period[1]).to.equalTime(period[1]); // upper bound
expect(newUser.course_period.inclusive).to.deep.equal([false, false]); // not inclusive expect(newUser.course_period.inclusive).to.deep.equal([true, false]); // inclusive, exclusive
// Check to see if updating a range field works // Check to see if updating a range field works
return newUser.updateAttributes({acceptable_marks: [0.8, 0.9]}).then(function() { return newUser.updateAttributes({acceptable_marks: [0.8, 0.9]}).then(function() {
...@@ -705,7 +705,7 @@ if (dialect.match(/^postgres/)) { ...@@ -705,7 +705,7 @@ if (dialect.match(/^postgres/)) {
expect(user.course_period[1] instanceof Date).to.be.ok; expect(user.course_period[1] instanceof Date).to.be.ok;
expect(user.course_period[0]).to.equalTime(period[0]); // lower bound expect(user.course_period[0]).to.equalTime(period[0]); // lower bound
expect(user.course_period[1]).to.equalTime(period[1]); // upper bound expect(user.course_period[1]).to.equalTime(period[1]); // upper bound
expect(user.course_period.inclusive).to.deep.equal([false, false]); // not inclusive expect(user.course_period.inclusive).to.deep.equal([true, false]); // inclusive, exclusive
}); });
}); });
}); });
...@@ -719,12 +719,12 @@ if (dialect.match(/^postgres/)) { ...@@ -719,12 +719,12 @@ if (dialect.match(/^postgres/)) {
expect(newUser.acceptable_marks.length).to.equal(2); expect(newUser.acceptable_marks.length).to.equal(2);
expect(newUser.acceptable_marks[0]).to.equal('0.65'); // lower bound expect(newUser.acceptable_marks[0]).to.equal('0.65'); // lower bound
expect(newUser.acceptable_marks[1]).to.equal('1'); // upper bound expect(newUser.acceptable_marks[1]).to.equal('1'); // upper bound
expect(newUser.acceptable_marks.inclusive).to.deep.equal([false, false]); // not inclusive expect(newUser.acceptable_marks.inclusive).to.deep.equal([true, false]); // inclusive, exclusive
expect(newUser.course_period[0] instanceof Date).to.be.ok; expect(newUser.course_period[0] instanceof Date).to.be.ok;
expect(newUser.course_period[1] instanceof Date).to.be.ok; expect(newUser.course_period[1] instanceof Date).to.be.ok;
expect(newUser.course_period[0]).to.equalTime(period[0]); // lower bound expect(newUser.course_period[0]).to.equalTime(period[0]); // lower bound
expect(newUser.course_period[1]).to.equalTime(period[1]); // upper bound expect(newUser.course_period[1]).to.equalTime(period[1]); // upper bound
expect(newUser.course_period.inclusive).to.deep.equal([false, false]); // not inclusive expect(newUser.course_period.inclusive).to.deep.equal([true, false]); // inclusive, exclusive
period = [new Date(2015, 1, 1), new Date(2015, 10, 30)]; period = [new Date(2015, 1, 1), new Date(2015, 10, 30)];
...@@ -735,7 +735,7 @@ if (dialect.match(/^postgres/)) { ...@@ -735,7 +735,7 @@ if (dialect.match(/^postgres/)) {
expect(newUser.course_period[1] instanceof Date).to.be.ok; expect(newUser.course_period[1] instanceof Date).to.be.ok;
expect(newUser.course_period[0]).to.equalTime(period[0]); // lower bound expect(newUser.course_period[0]).to.equalTime(period[0]); // lower bound
expect(newUser.course_period[1]).to.equalTime(period[1]); // upper bound expect(newUser.course_period[1]).to.equalTime(period[1]); // upper bound
expect(newUser.course_period.inclusive).to.deep.equal([false, false]); // not inclusive expect(newUser.course_period.inclusive).to.deep.equal([true, false]); // inclusive, exclusive
}); });
}); });
}); });
...@@ -758,7 +758,7 @@ if (dialect.match(/^postgres/)) { ...@@ -758,7 +758,7 @@ if (dialect.match(/^postgres/)) {
expect(users[0].course_period[1] instanceof Date).to.be.ok; expect(users[0].course_period[1] instanceof Date).to.be.ok;
expect(users[0].course_period[0]).to.equalTime(period[0]); // lower bound expect(users[0].course_period[0]).to.equalTime(period[0]); // lower bound
expect(users[0].course_period[1]).to.equalTime(period[1]); // upper bound expect(users[0].course_period[1]).to.equalTime(period[1]); // upper bound
expect(users[0].course_period.inclusive).to.deep.equal([false, false]); // not inclusive expect(users[0].course_period.inclusive).to.deep.equal([true, false]); // inclusive, exclusive
}); });
}); });
}); });
...@@ -821,10 +821,10 @@ if (dialect.match(/^postgres/)) { ...@@ -821,10 +821,10 @@ if (dialect.match(/^postgres/)) {
.then(function(users) { .then(function(users) {
expect(users[0].course_period[0]).to.equalTime(periods[0][0]); // lower bound expect(users[0].course_period[0]).to.equalTime(periods[0][0]); // lower bound
expect(users[0].course_period[1]).to.equalTime(periods[0][1]); // upper bound expect(users[0].course_period[1]).to.equalTime(periods[0][1]); // upper bound
expect(users[0].course_period.inclusive).to.deep.equal([false, false]); // not inclusive expect(users[0].course_period.inclusive).to.deep.equal([true, false]); // inclusive, exclusive
expect(users[1].course_period[0]).to.equalTime(periods[1][0]); // lower bound expect(users[1].course_period[0]).to.equalTime(periods[1][0]); // lower bound
expect(users[1].course_period[1]).to.equalTime(periods[1][1]); // upper bound expect(users[1].course_period[1]).to.equalTime(periods[1][1]); // upper bound
expect(users[1].course_period.inclusive).to.deep.equal([false, false]); // not inclusive expect(users[1].course_period.inclusive).to.deep.equal([true, false]); // inclusive, exclusive
}); });
}); });
......
...@@ -19,17 +19,17 @@ if (dialect.match(/^postgres/)) { ...@@ -19,17 +19,17 @@ if (dialect.match(/^postgres/)) {
}); });
it('should handle null as empty bound', function () { it('should handle null as empty bound', function () {
expect(range.stringify([null, 1])).to.equal('(,1)'); expect(range.stringify([null, 1])).to.equal('[,1)');
expect(range.stringify([1, null])).to.equal('(1,)'); expect(range.stringify([1, null])).to.equal('[1,)');
expect(range.stringify([null, null])).to.equal('(,)'); expect(range.stringify([null, null])).to.equal('[,)');
}); });
it('should handle Infinity/-Infinity as infinity/-infinity bounds', function () { it('should handle Infinity/-Infinity as infinity/-infinity bounds', function () {
expect(range.stringify([Infinity, 1])).to.equal('(infinity,1)'); expect(range.stringify([Infinity, 1])).to.equal('[infinity,1)');
expect(range.stringify([1, Infinity])).to.equal('(1,infinity)'); expect(range.stringify([1, Infinity])).to.equal('[1,infinity)');
expect(range.stringify([-Infinity, 1])).to.equal('(-infinity,1)'); expect(range.stringify([-Infinity, 1])).to.equal('[-infinity,1)');
expect(range.stringify([1, -Infinity])).to.equal('(1,-infinity)'); expect(range.stringify([1, -Infinity])).to.equal('[1,-infinity)');
expect(range.stringify([-Infinity, Infinity])).to.equal('(-infinity,infinity)'); expect(range.stringify([-Infinity, Infinity])).to.equal('[-infinity,infinity)');
}); });
it('should throw error when array length is no 0 or 2', function () { it('should throw error when array length is no 0 or 2', function () {
...@@ -47,7 +47,7 @@ if (dialect.match(/^postgres/)) { ...@@ -47,7 +47,7 @@ if (dialect.match(/^postgres/)) {
expect(range.stringify([{ inclusive: true, value: 0 }, { value: 1 }])).to.equal('[0,1)'); expect(range.stringify([{ inclusive: true, value: 0 }, { value: 1 }])).to.equal('[0,1)');
expect(range.stringify([{ inclusive: true, value: 0 }, { inclusive: true, value: 1 }])).to.equal('[0,1]'); expect(range.stringify([{ inclusive: true, value: 0 }, { inclusive: true, value: 1 }])).to.equal('[0,1]');
expect(range.stringify([{ inclusive: false, value: 0 }, 1])).to.equal('(0,1)'); expect(range.stringify([{ inclusive: false, value: 0 }, 1])).to.equal('(0,1)');
expect(range.stringify([0, { inclusive: true, value: 1 }])).to.equal('(0,1]'); expect(range.stringify([0, { inclusive: true, value: 1 }])).to.equal('[0,1]');
}); });
it('should handle inclusive property of input array properly', function () { it('should handle inclusive property of input array properly', function () {
...@@ -72,10 +72,39 @@ if (dialect.match(/^postgres/)) { ...@@ -72,10 +72,39 @@ if (dialect.match(/^postgres/)) {
it('should handle date values', function () { it('should handle date values', function () {
var Range = new DataTypes.postgres.RANGE(DataTypes.DATE); var Range = new DataTypes.postgres.RANGE(DataTypes.DATE);
expect(Range.stringify([new Date(Date.UTC(2000, 1, 1)), expect(Range.stringify([new Date(Date.UTC(2000, 1, 1)),
new Date(Date.UTC(2000, 1, 2))], { timezone: '+02:00' })).to.equal('\'("2000-02-01 02:00:00.000 +02:00","2000-02-02 02:00:00.000 +02:00")\''); new Date(Date.UTC(2000, 1, 2))], { timezone: '+02:00' })).to.equal('\'["2000-02-01 02:00:00.000 +02:00","2000-02-02 02:00:00.000 +02:00")\'');
}); });
}); });
describe('stringify value', function () {
it('should stringify integer values with appropriate casting', function () {
var Range = new DataTypes.postgres.RANGE(DataTypes.INTEGER);
expect(Range.stringify(1)).to.equal('\'1\'::integer');
});
it('should stringify bigint values with appropriate casting', function () {
var Range = new DataTypes.postgres.RANGE(DataTypes.BIGINT);
expect(Range.stringify(1)).to.equal('\'1\'::bigint');
});
it('should stringify numeric values with appropriate casting', function () {
var Range = new DataTypes.postgres.RANGE(DataTypes.DECIMAL);
expect(Range.stringify(1.1)).to.equal('\'1.1\'::numeric');
});
it('should stringify dateonly values with appropriate casting', function () {
var Range = new DataTypes.postgres.RANGE(DataTypes.DATEONLY);
expect(Range.stringify(new Date(Date.UTC(2000, 1, 1))).indexOf('::date')).not.to.equal(-1);
});
it('should stringify date values with appropriate casting', function () {
var Range = new DataTypes.postgres.RANGE(DataTypes.DATE);
expect(Range.stringify(new Date(Date.UTC(2000, 1, 1)), { timezone: '+02:00' })).to.equal('\'2000-02-01 02:00:00.000 +02:00\'::timestamptz');
});
});
describe('parse', function () { describe('parse', function () {
it('should handle a null object correctly', function () { it('should handle a null object correctly', function () {
expect(range.parse(null)).to.equal(null); expect(range.parse(null)).to.equal(null);
......
...@@ -610,6 +610,111 @@ suite(Support.getTestDialectTeaser('SQL'), function() { ...@@ -610,6 +610,111 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
}); });
} }
if (current.dialect.supports.RANGE) {
suite('RANGE', function () {
testsql('range', {
$contains: new Date(Date.UTC(2000, 1, 1))
}, {
field: {
type: new DataTypes.postgres.RANGE(DataTypes.DATE)
},
prefix: 'Timeline'
}, {
postgres: "\"Timeline\".\"range\" @> '2000-02-01 00:00:00.000 +00:00'::timestamptz"
});
testsql('range', {
$contains: [new Date(Date.UTC(2000, 1, 1)), new Date(Date.UTC(2000, 2, 1))]
}, {
field: {
type: new DataTypes.postgres.RANGE(DataTypes.DATE)
},
prefix: 'Timeline'
}, {
postgres: "\"Timeline\".\"range\" @> '[\"2000-02-01 00:00:00.000 +00:00\",\"2000-03-01 00:00:00.000 +00:00\")'"
});
testsql('range', {
$contained: [new Date(Date.UTC(2000, 1, 1)), new Date(Date.UTC(2000, 2, 1))]
}, {
field: {
type: new DataTypes.postgres.RANGE(DataTypes.DATE)
},
prefix: 'Timeline'
}, {
postgres: "\"Timeline\".\"range\" <@ '[\"2000-02-01 00:00:00.000 +00:00\",\"2000-03-01 00:00:00.000 +00:00\")'"
});
testsql('reservedSeats', {
$overlap: [1, 4]
}, {
field: {
type: new DataTypes.postgres.RANGE()
},
prefix: 'Room'
}, {
postgres: "\"Room\".\"reservedSeats\" && '[1,4)'"
});
testsql('reservedSeats', {
$adjacent: [1, 4]
}, {
field: {
type: new DataTypes.postgres.RANGE()
},
prefix: 'Room'
}, {
postgres: "\"Room\".\"reservedSeats\" -|- '[1,4)'"
});
testsql('reservedSeats', {
$strictLeft: [1, 4]
}, {
field: {
type: new DataTypes.postgres.RANGE()
},
prefix: 'Room'
}, {
postgres: "\"Room\".\"reservedSeats\" << '[1,4)'"
});
testsql('reservedSeats', {
$strictRight: [1, 4]
}, {
field: {
type: new DataTypes.postgres.RANGE()
},
prefix: 'Room'
}, {
postgres: "\"Room\".\"reservedSeats\" >> '[1,4)'"
});
testsql('reservedSeats', {
$noExtendRight: [1, 4]
}, {
field: {
type: new DataTypes.postgres.RANGE()
},
prefix: 'Room'
}, {
postgres: "\"Room\".\"reservedSeats\" &< '[1,4)'"
});
testsql('reservedSeats', {
$noExtendLeft: [1, 4]
}, {
field: {
type: new DataTypes.postgres.RANGE()
},
prefix: 'Room'
}, {
postgres: "\"Room\".\"reservedSeats\" &> '[1,4)'"
});
});
}
if (current.dialect.supports.JSON) { if (current.dialect.supports.JSON) {
suite('JSON', function () { suite('JSON', function () {
test('sequelize.json("profile->>\'id\', sequelize.cast(2, \'text\')")', function () { test('sequelize.json("profile->>\'id\', sequelize.cast(2, \'text\')")', function () {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!