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

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
......
'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!