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

Commit 440a168d by Dr. Evil

Support for range datatype for postgres

More details on range datatype: http://www.postgresql.org/docs/9.4/static/rangetypes.html
1 parent a0aa66b3
......@@ -328,7 +328,7 @@ util.inherits(JSONTYPE, ABSTRACT);
JSONTYPE.prototype.key = JSONTYPE.key = 'JSON';
/*
/**
* A pre-processed JSON data column. Only available in postgres.
* @property JSONB
*/
......@@ -382,6 +382,41 @@ BLOB.prototype.toSql = function() {
};
/**
* Range types are data types representing a range of values of some element type (called the range's subtype). Only available in postgres.
* See {@link http://www.postgresql.org/docs/9.4/static/rangetypes.html|Postgres documentation} for more details
* @property RANGE
*/
var RANGE = function (subtype) {
var options = {};
if(typeof subtype === 'function') { // if subtype passed - instantiate object of this subtype and return new function
options.subtype = new subtype();
return RANGE.bind({}, options);
}
else if(typeof subtype === 'object' && subtype.hasOwnProperty('subtype'))
options = subtype;
if (!(this instanceof RANGE)) return new RANGE(options);
ABSTRACT.apply(this, arguments);
this._subtype = options.subtype ? (options.subtype.key || 'INTEGER') : 'INTEGER';
};
util.inherits(RANGE, ABSTRACT);
var pgRangeSubtypes = {
integer: 'int4range',
bigint: 'int8range',
decimal: 'numrange',
dateonly: 'daterange',
date: 'tstzrange'
};
RANGE.prototype.key = RANGE.key = 'RANGE';
RANGE.prototype.toSql = function() {
return pgRangeSubtypes[this._subtype.toLowerCase()] || pgRangeSubtypes['integer'];
};
/**
* A column storing a unique univeral identifier. Use with `UUIDV1` or `UUIDV4` for default values.
* @property UUID
*/
......@@ -544,5 +579,6 @@ module.exports = {
VIRTUAL: VIRTUAL,
ARRAY: ARRAY,
NONE: VIRTUAL,
ENUM: ENUM
ENUM: ENUM,
RANGE: RANGE
};
......@@ -2,6 +2,7 @@
var Utils = require('../../utils')
, hstore = require('./hstore')
, range = require('./range')
, util = require('util')
, DataTypes = require('../../data-types')
, SqlString = require('../../sql-string')
......@@ -871,6 +872,13 @@ module.exports = (function() {
} else if (DataTypes.ARRAY.is(field.type, DataTypes.HSTORE)) {
return "ARRAY[" + Utils._.map(value, function(v){return "'" + hstore.stringify(v) + "'::hstore";}).join(",") + "]::HSTORE[]";
}
} else if(Utils._.isArray(value) && field && (field.type instanceof DataTypes.RANGE || DataTypes.ARRAY.is(field.type, DataTypes.RANGE))) {
if(field.type instanceof DataTypes.RANGE) { // escape single value
return "'" + range.stringify(value) + "'";
}
else if (DataTypes.ARRAY.is(field.type, DataTypes.RANGE)) { // escape array of ranges
return "ARRAY[" + Utils._.map(value, function(v){return "'" + range.stringify(v) + "'";}).join(",") + "]::" + field.type.toString();
}
} else if (field && (field.type instanceof DataTypes.JSON || field.type instanceof DataTypes.JSONB)) {
value = JSON.stringify(value);
} else if (Array.isArray(value) && field && DataTypes.ARRAY.is(field.type, DataTypes.JSON)) {
......
......@@ -4,6 +4,7 @@ var Utils = require('../../utils')
, AbstractQuery = require('../abstract/query')
, DataTypes = require('../../data-types')
, hstore = require('./hstore')
, range = require('./range')
, QueryTypes = require('../../query-types')
, Promise = require('../../promise')
, sequelizeErrors = require('../../errors.js');
......@@ -25,6 +26,20 @@ var parseHstoreFields = function(model, row) {
});
};
var parseRangeFields = function (model, row) {
Utils._.forEach(row, function (value, key) {
if (value === null) return row[key] = null;
if (model._isRangeAttribute(key)) {
row[key] = range.parse(value, model.attributes[key].type);
} else if (model.attributes[key] && DataTypes.ARRAY.is(model.attributes[key].type, DataTypes.RANGE)) {
var array = JSON.parse('[' + value.slice(1).slice(0, -1) + ']');
row[key] = Utils._.map(array, function (v) { return range.parse(v, model.attributes[key].type.type); });
} else {
row[key] = value;
}
});
};
module.exports = (function() {
var Query = function(client, sequelize, callee, options) {
......@@ -168,6 +183,12 @@ module.exports = (function() {
});
}
if (!!self.callee && !!self.callee._hasRangeAttributes) {
rows.forEach(function (row) {
parseRangeFields(self.callee, row);
});
}
return self.handleSelectQuery(rows);
} else if (QueryTypes.DESCRIBE === self.options.type) {
result = {};
......@@ -214,6 +235,12 @@ module.exports = (function() {
});
}
if (!!self.callee && !!self.callee._hasRangeAttributes) {
rows.forEach(function (row) {
parseRangeFields(self.callee, row);
});
}
return self.handleSelectQuery(rows);
} else if (QueryTypes.BULKDELETE === self.options.type) {
return parseInt(result.rowCount, 10);
......@@ -225,6 +252,10 @@ module.exports = (function() {
parseHstoreFields(self.callee.Model, rows[0]);
}
if (!!self.callee.Model && !!self.callee.Model._hasRangeAttributes) {
parseRangeFields(self.callee.Model, rows[0]);
}
for (var key in rows[0]) {
if (rows[0].hasOwnProperty(key)) {
var record = rows[0][key];
......
'use strict';
var Utils = require('../../utils'),
moment = require('moment');
module.exports = {
stringify: function (data) {
if (data === null) return null;
if (!Utils._.isArray(data) || data.length !== 2)
return '';
if (Utils._.any(data, Utils._.isNull))
return '';
if (data.hasOwnProperty('inclusive')) {
if (!data.inclusive)
data.inclusive = [false, false];
else if (data.inclusive === true)
data.inclusive = [true, true];
} else
data.inclusive = [false, false];
Utils._.each(data, function (value, index) {
if (Utils._.isObject(value)) {
if (value.hasOwnProperty('inclusive'))
data.inclusive[index] = !!value.inclusive;
if (value.hasOwnProperty('value'))
data[index] = value.value;
}
});
return (data.inclusive[0] ? '[' : '(') + JSON.stringify(data[0]) + ',' + JSON.stringify(data[1]) +
(data.inclusive[1] ? ']' : ')');
},
parse: function (value, attrType) {
if (value === null) return null;
if(typeof attrType === 'function') attrType = new attrType();
attrType = attrType || '';
var result = value
.slice(1, -1)
.split(',', 2);
if (result.length !== 2)
return value;
result = result
.map(function (value) {
switch (attrType.toString()) {
case 'int4range':
return parseInt(value, 10);
case 'numrange':
return parseFloat(value);
case 'daterange':
case 'tsrange':
case 'tstzrange':
return moment(value).toDate();
}
return value;
});
result.inclusive = [(value[0] === '['), (value[value.length - 1] === ']')];
return result;
}
};
......@@ -80,7 +80,7 @@ module.exports = (function() {
if (attribute.type === undefined) {
throw new Error('Unrecognized data type for field ' + name);
}
}
if (attribute.type instanceof DataTypes.ENUM) {
if (!attribute.values.length) {
......@@ -156,7 +156,7 @@ module.exports = (function() {
self.options.uniqueKeys[idxName] = {
name: idxName,
fields: [attribute],
singleField: true,
singleField: true
};
} else if (options.unique !== false) {
idxName = options.unique;
......@@ -281,6 +281,7 @@ module.exports = (function() {
this._booleanAttributes = [];
this._dateAttributes = [];
this._hstoreAttributes = [];
this._rangeAttributes = [];
this._jsonAttributes = [];
this._virtualAttributes = [];
this._defaultValues = {};
......@@ -303,6 +304,8 @@ module.exports = (function() {
self._dateAttributes.push(name);
} else if (definition.type instanceof DataTypes.HSTORE) {
self._hstoreAttributes.push(name);
} else if (definition.type instanceof DataTypes.RANGE) {
self._rangeAttributes.push(name);
} else if (definition.type instanceof DataTypes.JSON) {
self._jsonAttributes.push(name);
} else if (definition.type instanceof DataTypes.VIRTUAL) {
......@@ -313,7 +316,7 @@ module.exports = (function() {
if (typeof definition.defaultValue === "function" && (
definition.defaultValue === DataTypes.NOW ||
definition.defaultValue === DataTypes.UUIDV4 ||
definition.defaultValue === DataTypes.UUIDV4
definition.defaultValue === DataTypes.UUIDV4
)) {
definition.defaultValue = new definition.defaultValue();
}
......@@ -341,6 +344,11 @@ module.exports = (function() {
return self._hstoreAttributes.indexOf(key) !== -1;
});
this._hasRangeAttributes = !!this._rangeAttributes.length;
this._isRangeAttribute = Utils._.memoize(function(key) {
return self._rangeAttributes.indexOf(key) !== -1;
});
this._hasJsonAttributes = !!this._jsonAttributes.length;
this._isJsonAttribute = Utils._.memoize(function(key) {
return self._jsonAttributes.indexOf(key) !== -1;
......@@ -819,7 +827,7 @@ module.exports = (function() {
}
options = paranoidClause(this, options);
return this.QueryInterface.rawSelect(this.getTableName(options), options, aggregateFunction, this);
};
......@@ -1141,7 +1149,7 @@ module.exports = (function() {
queryOptions.transaction = transaction;
return self.find(options, {
transaction: transaction,
transaction: transaction
});
}).then(function(instance) {
if (instance !== null) {
......@@ -1171,7 +1179,7 @@ module.exports = (function() {
// Someone must have created a matching instance inside the same transaction since we last did a find. Let's find it!
return self.find(options, {
transaction: internalTransaction ? null : this.transaction,
transaction: internalTransaction ? null : this.transaction
}).then(function(instance) {
// Sanity check, ideally we caught this at the defaultFeilds/err.fields check
// But if we didn't and instance is null, we will throw
......
......@@ -24,7 +24,11 @@ if (dialect.match(/^postgres/)) {
friends: {
type: DataTypes.ARRAY(DataTypes.JSON),
defaultValue: []
}
},
course_period: DataTypes.RANGE(DataTypes.DATE),
acceptable_marks: { type: DataTypes.RANGE(DataTypes.DECIMAL), defaultValue: [0.65, 1] },
available_amount: DataTypes.RANGE,
holidays: DataTypes.ARRAY(DataTypes.RANGE(DataTypes.DATE))
});
this.User.sync({ force: true }).success(function() {
done();
......@@ -37,8 +41,8 @@ if (dialect.match(/^postgres/)) {
});
it('should be able to search within an array', function(done) {
this.User.all({where: {email: ['hello', 'world']}}).on('sql', function(sql) {
expect(sql).to.equal('SELECT "id", "username", "email", "settings", "document", "phones", "emergency_contact", "friends", "createdAt", "updatedAt" FROM "Users" AS "User" WHERE "User"."email" = ARRAY[\'hello\',\'world\']::TEXT[];');
this.User.all({where: {email: ['hello', 'world']}, attributes: ['id','username','email','settings','document','phones','emergency_contact','friends']}).on('sql', function(sql) {
expect(sql).to.equal('SELECT "id", "username", "email", "settings", "document", "phones", "emergency_contact", "friends" FROM "Users" AS "User" WHERE "User"."email" = ARRAY[\'hello\',\'world\']::TEXT[];');
done();
});
});
......@@ -301,6 +305,27 @@ if (dialect.match(/^postgres/)) {
});
describe('range', function() {
it('should tell me that a column is range and not USER-DEFINED', function() {
return this.sequelize.queryInterface.describeTable('Users').then(function(table) {
expect(table.course_period.type).to.equal('TSTZRANGE');
expect(table.available_amount.type).to.equal('INT4RANGE');
});
});
it('should stringify range with insert', function() {
return this.User.create({
username: 'bob',
email: ['myemail@email.com'],
course_period: [{value: new Date(2015,0,1), inclusive: true}, {value: new Date(2015,11,31), inclusive: true}]
}).on('sql', function(sql) {
var expected = '["2015-01-01T00:00:00.000Z","2015-12-31T00:00:00.000Z"]';
expect(sql.indexOf(expected)).not.to.equal(-1);
});
});
});
describe('enums', function() {
it('should be able to ignore enum types that already exist', function(done) {
var User = this.sequelize.define('UserEnums', {
......@@ -626,6 +651,172 @@ if (dialect.match(/^postgres/)) {
})
.error(console.log);
});
it('should save range correctly', function() {
return this.User.create({ username: 'user', email: ['foo@bar.com'], course_period: [new Date(2015, 0, 1), new Date(2015, 11, 31)]}).then(function(newUser) {
// Check to see if the default value for a range field works
expect(newUser.acceptable_marks.length).to.equal(2);
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.inclusive).to.deep.equal([false, false]); // not inclusive
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[0].toISOString()).to.equal('2015-01-01T00:00:00.000Z'); // lower bound
expect(newUser.course_period[1].toISOString()).to.equal('2015-12-31T00:00:00.000Z'); // upper bound
expect(newUser.course_period.inclusive).to.deep.equal([false, false]); // not inclusive
// Check to see if updating a range field works
return newUser.updateAttributes({acceptable_marks: [0.8, 0.9]}).then(function(oldUser) {
expect(newUser.acceptable_marks.length).to.equal(2);
expect(newUser.acceptable_marks[0]).to.equal(0.8); // lower bound
expect(newUser.acceptable_marks[1]).to.equal(0.9); // upper bound
});
});
});
it('should save range array correctly', function() {
var User = this.User;
return this.User.create({
username: 'bob',
email: ['myemail@email.com'],
holidays: [[new Date(2015, 3, 1), new Date(2015, 3, 15)], [new Date(2015, 8, 1), new Date(2015, 9, 15)]]
}).then(function() {
return User.find(1).then(function(user) {
expect(user.holidays.length).to.equal(2);
expect(user.holidays[0].length).to.equal(2);
expect(user.holidays[0][0] instanceof Date).to.be.ok;
expect(user.holidays[0][1] instanceof Date).to.be.ok;
expect(user.holidays[0][0].toISOString()).to.equal("2015-03-31T23:00:00.000Z");
expect(user.holidays[0][1].toISOString()).to.equal("2015-04-14T23:00:00.000Z");
expect(user.holidays[1].length).to.equal(2);
expect(user.holidays[1][0] instanceof Date).to.be.ok;
expect(user.holidays[1][1] instanceof Date).to.be.ok;
expect(user.holidays[1][0].toISOString()).to.equal("2015-08-31T23:00:00.000Z");
expect(user.holidays[1][1].toISOString()).to.equal("2015-10-14T23:00:00.000Z");
});
});
});
it('should bulkCreate with range property', function() {
var User = this.User;
return this.User.bulkCreate([{
username: 'bob',
email: ['myemail@email.com'],
course_period: [new Date(2015, 0, 1), new Date(2015, 11, 31)]
}]).then(function() {
return User.find(1).then(function(user) {
expect(user.course_period[0] instanceof Date).to.be.ok;
expect(user.course_period[1] instanceof Date).to.be.ok;
expect(user.course_period[0].toISOString()).to.equal('2015-01-01T00:00:00.000Z'); // lower bound
expect(user.course_period[1].toISOString()).to.equal('2015-12-31T00:00:00.000Z'); // upper bound
expect(user.course_period.inclusive).to.deep.equal([false, false]); // not inclusive
});
});
});
it('should update range correctly', function() {
var self = this;
return this.User.create({ username: 'user', email: ['foo@bar.com'], course_period: [new Date(2015, 0, 1), new Date(2015, 11, 31)]}).then(function(newUser) {
// Check to see if the default value for a range field works
expect(newUser.acceptable_marks.length).to.equal(2);
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.inclusive).to.deep.equal([false, false]); // not inclusive
expect(newUser.course_period[0] instanceof Date).to.be.ok;
expect(newUser.course_period[1] instanceof Date).to.be.ok;
expect(newUser.course_period[0].toISOString()).to.equal('2015-01-01T00:00:00.000Z'); // lower bound
expect(newUser.course_period[1].toISOString()).to.equal('2015-12-31T00:00:00.000Z'); // upper bound
expect(newUser.course_period.inclusive).to.deep.equal([false, false]); // not inclusive
// Check to see if updating a range field works
return self.User.update({course_period: [new Date(2015, 1, 1), new Date(2015, 10, 30)]}, {where: newUser.identifiers}).then(function() {
return newUser.reload().success(function() {
expect(newUser.course_period[0] instanceof Date).to.be.ok;
expect(newUser.course_period[1] instanceof Date).to.be.ok;
expect(newUser.course_period[0].toISOString()).to.equal('2015-02-01T00:00:00.000Z'); // lower bound
expect(newUser.course_period[1].toISOString()).to.equal('2015-11-30T00:00:00.000Z'); // upper bound
expect(newUser.course_period.inclusive).to.deep.equal([false, false]); // not inclusive
});
});
});
});
it('should update range correctly and return the affected rows', function() {
var self = this;
return this.User.create({ username: 'user', email: ['foo@bar.com'], course_period: [new Date(2015, 0, 1), new Date(2015, 11, 31)]}).then(function(oldUser) {
// Update the user and check that the returned object's fields have been parsed by the range parser
return self.User.update({course_period: [new Date(2015, 1, 1), new Date(2015, 10, 30)]}, {where: oldUser.identifiers, returning: true }).spread(function(count, users) {
expect(count).to.equal(1);
expect(users[0].course_period[0] instanceof Date).to.be.ok;
expect(users[0].course_period[1] instanceof Date).to.be.ok;
expect(users[0].course_period[0].toISOString()).to.equal('2015-02-01T00:00:00.000Z'); // lower bound
expect(users[0].course_period[1].toISOString()).to.equal('2015-11-30T00:00:00.000Z'); // upper bound
expect(users[0].course_period.inclusive).to.deep.equal([false, false]); // not inclusive
});
});
});
it('should read range correctly', function() {
var self = this;
var course_period = [new Date(2015, 1, 1), new Date(2015, 10, 30)];
course_period.inclusive = [false, false];
var data = { username: 'user', email: ['foo@bar.com'], course_period: course_period};
return this.User.create(data)
.then(function() {
return self.User.find({ where: { username: 'user' }});
})
.then(function(user) {
// Check that the range fields are the same when retrieving the user
expect(user.course_period).to.deep.equal(data.course_period);
});
});
it('should read range array correctly', function() {
var self = this;
var holidays = [[new Date(2015, 3, 1, 10), new Date(2015, 3, 15)],[new Date(2015, 8, 1), new Date(2015, 9, 15)]];
holidays[0].inclusive = [true, true];
holidays[1].inclusive = [true, true];
var data = { username: 'user', email: ['foo@bar.com'], holidays: holidays };
return this.User.create(data)
.then(function() {
// Check that the range fields are the same when retrieving the user
return self.User.find({ where: { username: 'user' }});
}).then(function(user) {
expect(user.holidays).to.deep.equal(data.holidays);
});
});
it('should read range correctly from multiple rows', function(done) {
var self = this;
self.User
.create({ username: 'user1', email: ['foo@bar.com'], course_period: [new Date(2015, 0, 1), new Date(2015, 11, 31)]})
.then(function() {
return self.User.create({ username: 'user2', email: ['foo2@bar.com'], course_period: [new Date(2016, 0, 1), new Date(2016, 11, 31)]});
})
.then(function() {
// Check that the range fields are the same when retrieving the user
return self.User.findAll({ order: 'username' });
})
.then(function(users) {
expect(users[0].course_period[0].toISOString()).to.equal('2015-01-01T00:00:00.000Z'); // lower bound
expect(users[0].course_period[1].toISOString()).to.equal('2015-12-31T00:00:00.000Z'); // upper bound
expect(users[0].course_period.inclusive).to.deep.equal([false, false]); // not inclusive
expect(users[1].course_period[0].toISOString()).to.equal('2016-01-01T00:00:00.000Z'); // lower bound
expect(users[1].course_period[1].toISOString()).to.equal('2016-12-31T00:00:00.000Z'); // upper bound
expect(users[1].course_period.inclusive).to.deep.equal([false, false]); // not inclusive
done();
})
.error(console.log);
});
});
describe('[POSTGRES] Unquoted identifiers', function() {
......
'use strict';
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/../../support')
, DataTypes = require(__dirname + '/../../../../lib/data-types')
, dialect = Support.getTestDialect()
, range = require('../../../../lib/dialects/postgres/range');
chai.config.includeStack = true;
if (dialect.match(/^postgres/)) {
describe('[POSTGRES Specific] range datatype', function () {
describe('stringify', function () {
it('should handle empty objects correctly', function () {
expect(range.stringify([])).to.equal('');
});
it('should return empty string when either of boundaries is null', function () {
expect(range.stringify([null, "test"])).to.equal('');
expect(range.stringify([123, null])).to.equal('');
});
it('should return empty string when boundaries array of invalid size', function () {
expect(range.stringify([1])).to.equal('');
expect(range.stringify([1, 2, 3])).to.equal('');
});
it('should return empty string when non-array parameter is passed', function (done) {
expect(range.stringify({})).to.equal('');
expect(range.stringify('test')).to.equal('');
expect(range.stringify(undefined)).to.equal('');
done();
});
it('should handle array of objects with `inclusive` and `value` properties', function () {
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: false, value: 0 }, 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 () {
var testRange = [1, 2];
testRange.inclusive = [true, false];
expect(range.stringify(testRange)).to.equal('[1,2)');
testRange.inclusive = [false, true];
expect(range.stringify(testRange)).to.equal('(1,2]');
testRange.inclusive = [true, true];
expect(range.stringify(testRange)).to.equal('[1,2]');
testRange.inclusive = true;
expect(range.stringify(testRange)).to.equal('[1,2]');
testRange.inclusive = false;
expect(range.stringify(testRange)).to.equal('(1,2)');
});
it('should handle date values', function () {
expect(range.stringify([new Date(2000, 1, 1),
new Date(2000, 1, 2)])).to.equal('("2000-02-01T00:00:00.000Z","2000-02-02T00:00:00.000Z")');
});
});
describe('parse', function () {
it('should handle a null object correctly', function () {
expect(range.parse(null)).to.equal(null);
});
it('should handle empty string correctly', function () {
expect(range.parse('')).to.deep.equal('');
});
it('should return raw value if not range is returned', function () {
expect(range.parse('some_non_array')).to.deep.equal('some_non_array');
});
});
describe('stringify and parse', function () {
it('should stringify then parse back the same structure', function () {
var testRange = [5,10];
testRange.inclusive = [true, true];
expect(range.parse(range.stringify(testRange), DataTypes.RANGE(DataTypes.INTEGER))).to.deep.equal(testRange);
expect(range.parse(range.stringify(range.parse(range.stringify(testRange), DataTypes.RANGE(DataTypes.INTEGER))), DataTypes.RANGE(DataTypes.INTEGER))).to.deep.equal(testRange);
});
});
});
}
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!