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

Commit 88e751d3 by Mick Hansen

Merge pull request #1998 from seegno/add-hstore-array-support

Add hstore array support
2 parents e59ea76b f13ec84c
...@@ -17,13 +17,23 @@ module.exports = { ...@@ -17,13 +17,23 @@ module.exports = {
return '"' + JSON.stringify(part).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; return '"' + JSON.stringify(part).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
} }
}, },
stringify: function(data) { stringifyObject: function(data) {
var self = this; var self = this;
return Object.keys(data).map(function(key) { return Object.keys(data).map(function(key) {
return self.stringifyPart(key) + '=>' + self.stringifyPart(data[key]); return self.stringifyPart(key) + '=>' + self.stringifyPart(data[key]);
}).join(','); }).join(',');
}, },
stringifyArray: function(data) {
return data.map(this.stringifyObject, this);
},
stringify: function(data) {
if (Array.isArray(data)) {
return this.stringifyArray(data);
}
return this.stringifyObject(data);
},
parsePart: function(part) { parsePart: function(part) {
part = part.replace(/\\\\/g, '\\').replace(/\\"/g, '"'); part = part.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
...@@ -35,11 +45,11 @@ module.exports = { ...@@ -35,11 +45,11 @@ module.exports = {
return part; return part;
} }
}, },
parse: function(string) { parseObject: function(string) {
var self = this, var self = this,
object = { }; object = { };
if (('string' !== typeof string) || (0 === string.length)) { if (0 === string.length) {
return object; return object;
} }
...@@ -65,5 +75,22 @@ module.exports = { ...@@ -65,5 +75,22 @@ module.exports = {
}); });
return object; return object;
},
parseArray: function(string) {
var matches = string.match(/{(.*)}/);
var array = JSON.parse('['+ matches[1] +']');
return array.map(this.parseObject, this);
},
parse: function(value) {
if ('string' !== typeof value) {
return value;
}
if ('{' === value[0] && '}' === value[value.length - 1]) {
return this.parseArray(value);
}
return this.parseObject(value);
} }
}; };
...@@ -439,12 +439,12 @@ module.exports = (function() { ...@@ -439,12 +439,12 @@ module.exports = (function() {
} }
} }
if (dataType.type === 'TINYINT(1)') { if (dataType.type === DataTypes.BOOLEAN) {
dataType.type = 'BOOLEAN'; dataType.type = 'BOOLEAN';
} }
if (dataType.type === 'DATETIME') { if (dataType.type === DataTypes.DATE) {
dataType.originalType = 'DATETIME'; dataType._typeName = 'DATETIME';
dataType.type = 'TIMESTAMP WITH TIME ZONE'; dataType.type = 'TIMESTAMP WITH TIME ZONE';
} }
...@@ -823,13 +823,13 @@ module.exports = (function() { ...@@ -823,13 +823,13 @@ module.exports = (function() {
escape: function(value, field) { escape: function(value, field) {
if (value && value._isSequelizeMethod) { if (value && value._isSequelizeMethod) {
return value.toString(this); return value.toString(this);
} else { }
if (Utils._.isObject(value) && field && (field === DataTypes.HSTORE || field.type === DataTypes.HSTORE)) {
value = hstore.stringify(value);
}
return SqlString.escape(value, false, null, this.dialect, field); if (Utils._.isObject(value) && field && (field.type === DataTypes.HSTORE || field.type === DataTypes.ARRAY(DataTypes.HSTORE))) {
value = hstore.stringify(value);
} }
return SqlString.escape(value, false, null, this.dialect, field);
}, },
/** /**
......
...@@ -10,10 +10,14 @@ var Utils = require('../../utils') ...@@ -10,10 +10,14 @@ var Utils = require('../../utils')
// Parses hstore fields if the model has any hstore fields. // Parses hstore fields if the model has any hstore fields.
// This cannot be done in the 'pg' lib because hstore is a UDT. // This cannot be done in the 'pg' lib because hstore is a UDT.
var parseHstoreFields = function(model, row) { var parseHstoreFields = function(model, row) {
Utils._.keys(row).forEach(function(key) { Utils._.forEach(row, function(value, key) {
if (model._isHstoreAttribute(key)) { if (model._isHstoreAttribute(key) || (model.attributes[key] && model.attributes[key].type === DataTypes.ARRAY(DataTypes.HSTORE))) {
row[key] = hstore.parse(row[key]); row[key] = hstore.parse(value);
return;
} }
row[key] = value;
}); });
}; };
......
...@@ -475,7 +475,7 @@ module.exports = (function() { ...@@ -475,7 +475,7 @@ module.exports = (function() {
for (var attrName in self.Model.rawAttributes) { for (var attrName in self.Model.rawAttributes) {
if (self.Model.rawAttributes.hasOwnProperty(attrName)) { if (self.Model.rawAttributes.hasOwnProperty(attrName)) {
var definition = self.Model.rawAttributes[attrName] var definition = self.Model.rawAttributes[attrName]
, isEnum = !!definition.type && (definition.type.toString() === DataTypes.ENUM.toString()) , isEnum = definition.type.toString() === DataTypes.ENUM.toString()
, isMySQL = ['mysql', 'mariadb'].indexOf(self.Model.modelManager.sequelize.options.dialect) !== -1 , isMySQL = ['mysql', 'mariadb'].indexOf(self.Model.modelManager.sequelize.options.dialect) !== -1
, ciCollation = !!self.Model.options.collate && self.Model.options.collate.match(/_ci$/i) , ciCollation = !!self.Model.options.collate && self.Model.options.collate.match(/_ci$/i)
, valueOutOfScope; , valueOutOfScope;
......
...@@ -45,9 +45,20 @@ module.exports = (function() { ...@@ -45,9 +45,20 @@ module.exports = (function() {
} }
}, options || {}); }, options || {});
this.associations = {};
this.modelManager = this.daoFactoryManager = null;
this.name = name;
this.options.hooks = this.replaceHookAliases(this.options.hooks);
this.scopeObj = {};
this.sequelize = options.sequelize; this.sequelize = options.sequelize;
this.underscored = this.underscored || this.underscoredAll; this.underscored = this.underscored || this.underscoredAll;
if (!this.options.tableName) {
this.tableName = this.options.freezeTableName ? name : Utils._.underscoredIf(Utils.pluralize(name, this.options.language), this.options.underscoredAll);
} else {
this.tableName = this.options.tableName;
}
// error check options // error check options
Utils._.each(options.validate, function(validator, validatorType) { Utils._.each(options.validate, function(validator, validatorType) {
if (Utils._.contains(Utils._.keys(attributes), validatorType)) { if (Utils._.contains(Utils._.keys(attributes), validatorType)) {
...@@ -59,38 +70,27 @@ module.exports = (function() { ...@@ -59,38 +70,27 @@ module.exports = (function() {
} }
}); });
this.name = name; this.attributes = this.rawAttributes = Utils._.mapValues(attributes, function(attribute, name) {
if (!Utils._.isPlainObject(attribute)) {
if (!this.options.tableName) { attribute = { type: attribute };
this.tableName = this.options.freezeTableName ? name : Utils._.underscoredIf(Utils.pluralize(name, this.options.language), this.options.underscoredAll); }
} else {
this.tableName = this.options.tableName;
}
// If you don't specify a valid data type lets help you debug it if (attribute.references instanceof Model) {
Utils._.each(attributes, function(attribute, name) { attribute.references = attribute.references.tableName;
var dataType;
if (Utils._.isPlainObject(attribute)) {
// We have special cases where the type is an object containing
// the values (e.g. Sequelize.ENUM(value, value2) returns an object
// instead of a function)
// Copy these values to the dataType
attribute.values = (attribute.type && attribute.type.values) || attribute.values;
// We keep on working with the actual type object
dataType = attribute.type;
} else {
dataType = attribute;
} }
if (dataType === undefined) { if (attribute.type === undefined) {
throw new Error('Unrecognized data type for field ' + name); throw new Error('Unrecognized data type for field ' + name);
} }
if (dataType.toString() === 'ENUM') { if (attribute.type.toString() === DataTypes.ENUM.toString()) {
if (!(Array.isArray(attribute.values) && (attribute.values.length > 0))) { // The ENUM is a special case where the type is an object containing the values
attribute.values = attribute.type.values || attribute.values || [];
if (!attribute.values.length) {
throw new Error('Values for ENUM haven\'t been defined.'); throw new Error('Values for ENUM haven\'t been defined.');
} }
attributes[name].validate = attributes[name].validate || { attributes[name].validate = attributes[name].validate || {
_checkEnum: function(value, next) { _checkEnum: function(value, next) {
var hasValue = value !== undefined var hasValue = value !== undefined
...@@ -113,22 +113,9 @@ module.exports = (function() { ...@@ -113,22 +113,9 @@ module.exports = (function() {
} }
}; };
} }
});
Object.keys(attributes).forEach(function(attrName) { return attribute;
if (attributes[attrName].references instanceof Model) {
attributes[attrName].references = attributes[attrName].references.tableName;
}
}); });
this.options.hooks = this.replaceHookAliases(this.options.hooks);
this.attributes =
this.rawAttributes = attributes;
this.modelManager =
this.daoFactoryManager = null; // defined in init function
this.associations = {};
this.scopeObj = {};
}; };
Object.defineProperty(Model.prototype, 'QueryInterface', { Object.defineProperty(Model.prototype, 'QueryInterface', {
...@@ -263,7 +250,7 @@ module.exports = (function() { ...@@ -263,7 +250,7 @@ module.exports = (function() {
this.Instance.prototype.validators = {}; this.Instance.prototype.validators = {};
Utils._.each(this.rawAttributes, function(definition, name) { Utils._.each(this.rawAttributes, function(definition, name) {
var type = definition.originalType || definition.type || definition; var type = definition.type._typeName || definition.type;
if (type === DataTypes.BOOLEAN) { if (type === DataTypes.BOOLEAN) {
self._booleanAttributes.push(name); self._booleanAttributes.push(name);
...@@ -831,7 +818,7 @@ module.exports = (function() { ...@@ -831,7 +818,7 @@ module.exports = (function() {
if (!options.dataType) { if (!options.dataType) {
if (this.rawAttributes[field]) { if (this.rawAttributes[field]) {
options.dataType = this.rawAttributes[field]; options.dataType = this.rawAttributes[field].type;
} else { } else {
// Use FLOAT as fallback // Use FLOAT as fallback
options.dataType = DataTypes.FLOAT; options.dataType = DataTypes.FLOAT;
......
...@@ -62,21 +62,19 @@ module.exports = (function() { ...@@ -62,21 +62,19 @@ module.exports = (function() {
}; };
QueryInterface.prototype.createTable = function(tableName, attributes, options) { QueryInterface.prototype.createTable = function(tableName, attributes, options) {
var attributeHashes = {} var keys = Object.keys(attributes)
, dataTypeValues = Utils._.values(DataTypes)
, keys = Object.keys(attributes)
, keyLen = keys.length , keyLen = keys.length
, self = this , self = this
, sql = '' , sql = ''
, i = 0; , i = 0;
for (i = 0; i < keyLen; i++) { attributes = Utils._.mapValues(attributes, function(attribute, name) {
if (dataTypeValues.indexOf(attributes[keys[i]]) > -1) { if (!Utils._.isPlainObject(attribute)) {
attributeHashes[keys[i]] = { type: attributes[keys[i]], allowNull: true }; attribute = { type: attribute, allowNull: true };
} else {
attributeHashes[keys[i]] = attributes[keys[i]];
} }
}
return attribute;
});
options = Utils._.extend({ options = Utils._.extend({
logging: this.sequelize.options.logging logging: this.sequelize.options.logging
...@@ -90,7 +88,7 @@ module.exports = (function() { ...@@ -90,7 +88,7 @@ module.exports = (function() {
, getTableName = (!options || !options.schema || options.schema === 'public' ? '' : options.schema + '_') + tableName; , getTableName = (!options || !options.schema || options.schema === 'public' ? '' : options.schema + '_') + tableName;
for (i = 0; i < keyLen; i++) { for (i = 0; i < keyLen; i++) {
if (attributes[keys[i]].toString().match(/^ENUM\(/) || attributes[keys[i]].toString() === 'ENUM' || (attributes[keys[i]].type && attributes[keys[i]].type.toString() === 'ENUM')) { if (attributes[keys[i]].type.toString() === DataTypes.ENUM.toString()) {
sql = self.QueryGenerator.pgListEnums(getTableName, keys[i], options); sql = self.QueryGenerator.pgListEnums(getTableName, keys[i], options);
promises.push(self.sequelize.query(sql, null, { plain: true, raw: true, type: QueryTypes.SELECT, logging: options.logging })); promises.push(self.sequelize.query(sql, null, { plain: true, raw: true, type: QueryTypes.SELECT, logging: options.logging }));
} }
...@@ -105,7 +103,7 @@ module.exports = (function() { ...@@ -105,7 +103,7 @@ module.exports = (function() {
daoTable = daoTable.length > 0 ? daoTable[0] : null; daoTable = daoTable.length > 0 ? daoTable[0] : null;
for (i = 0; i < keyLen; i++) { for (i = 0; i < keyLen; i++) {
if (attributes[keys[i]].toString().match(/^ENUM\(/) || attributes[keys[i]].toString() === 'ENUM' || (attributes[keys[i]].type && attributes[keys[i]].type.toString() === 'ENUM')) { if (attributes[keys[i]].type.toString() === DataTypes.ENUM.toString()) {
// If the enum type doesn't exist then create it // If the enum type doesn't exist then create it
if (!results[enumIdx]) { if (!results[enumIdx]) {
sql = self.QueryGenerator.pgEnum(getTableName, keys[i], attributes[keys[i]], options); sql = self.QueryGenerator.pgEnum(getTableName, keys[i], attributes[keys[i]], options);
...@@ -135,7 +133,7 @@ module.exports = (function() { ...@@ -135,7 +133,7 @@ module.exports = (function() {
} }
} }
attributes = self.QueryGenerator.attributesToSQL(attributeHashes); attributes = self.QueryGenerator.attributesToSQL(attributes);
sql = self.QueryGenerator.createTableQuery(tableName, attributes, options); sql = self.QueryGenerator.createTableQuery(tableName, attributes, options);
return Promise.all(promises).then(function() { return Promise.all(promises).then(function() {
...@@ -143,7 +141,7 @@ module.exports = (function() { ...@@ -143,7 +141,7 @@ module.exports = (function() {
}); });
}); });
} else { } else {
attributes = self.QueryGenerator.attributesToSQL(attributeHashes); attributes = self.QueryGenerator.attributesToSQL(attributes);
sql = self.QueryGenerator.createTableQuery(tableName, attributes, options); sql = self.QueryGenerator.createTableQuery(tableName, attributes, options);
return self.sequelize.query(sql, null, options); return self.sequelize.query(sql, null, options);
...@@ -175,7 +173,7 @@ module.exports = (function() { ...@@ -175,7 +173,7 @@ module.exports = (function() {
, i = 0; , i = 0;
for (i = 0; i < keyLen; i++) { for (i = 0; i < keyLen; i++) {
if (daoTable.rawAttributes[keys[i]].type && daoTable.rawAttributes[keys[i]].type.toString() === 'ENUM') { if (daoTable.rawAttributes[keys[i]].type.toString() === DataTypes.ENUM.toString()) {
promises.push(self.sequelize.query(self.QueryGenerator.pgEnumDrop(getTableName, keys[i]), null, {logging: options.logging, raw: true})); promises.push(self.sequelize.query(self.QueryGenerator.pgEnumDrop(getTableName, keys[i]), null, {logging: options.logging, raw: true}));
} }
} }
......
...@@ -15,7 +15,8 @@ if (dialect.match(/^postgres/)) { ...@@ -15,7 +15,8 @@ if (dialect.match(/^postgres/)) {
username: DataTypes.STRING, username: DataTypes.STRING,
email: { type: DataTypes.ARRAY(DataTypes.TEXT) }, email: { type: DataTypes.ARRAY(DataTypes.TEXT) },
settings: DataTypes.HSTORE, settings: DataTypes.HSTORE,
document: { type: DataTypes.HSTORE, defaultValue: { default: 'value' } } document: { type: DataTypes.HSTORE, defaultValue: { default: 'value' } },
phones: DataTypes.ARRAY(DataTypes.HSTORE)
}) })
this.User.sync({ force: true }).success(function() { this.User.sync({ force: true }).success(function() {
done() done()
...@@ -30,7 +31,7 @@ if (dialect.match(/^postgres/)) { ...@@ -30,7 +31,7 @@ if (dialect.match(/^postgres/)) {
it('should be able to search within an array', function(done) { it('should be able to search within an array', function(done) {
this.User.all({where: {email: ['hello', 'world']}}).on('sql', function(sql) { this.User.all({where: {email: ['hello', 'world']}}).on('sql', function(sql) {
expect(sql).to.equal('SELECT "id", "username", "email", "settings", "document", "createdAt", "updatedAt" FROM "Users" AS "User" WHERE "User"."email" && ARRAY[\'hello\',\'world\']::TEXT[];') expect(sql).to.equal('SELECT "id", "username", "email", "settings", "document", "phones", "createdAt", "updatedAt" FROM "Users" AS "User" WHERE "User"."email" && ARRAY[\'hello\',\'world\']::TEXT[];')
done() done()
}) })
}) })
...@@ -273,6 +274,18 @@ if (dialect.match(/^postgres/)) { ...@@ -273,6 +274,18 @@ if (dialect.match(/^postgres/)) {
.error(console.log) .error(console.log)
}) })
it('should save hstore array correctly', function(done) {
this.User.create({
username: 'bob',
email: ['myemail@email.com'],
phones: [{ number: '123456789', type: 'mobile' }, { number: '987654321', type: 'landline' }]
}).on('sql', function(sql) {
var expected = 'INSERT INTO "Users" ("id","username","email","document","phones","createdAt","updatedAt") VALUES (DEFAULT,\'bob\',ARRAY[\'myemail@email.com\']::TEXT[],\'"default"=>"value"\',ARRAY[\'"number"=>"123456789","type"=>"mobile"\',\'"number"=>"987654321","type"=>"landline"\']::HSTORE[]'
expect(sql).to.contain(expected)
done()
})
})
it("should update hstore correctly", function(done) { it("should update hstore correctly", function(done) {
var self = this var self = this
...@@ -329,6 +342,23 @@ if (dialect.match(/^postgres/)) { ...@@ -329,6 +342,23 @@ if (dialect.match(/^postgres/)) {
.error(console.log) .error(console.log)
}) })
it('should read an hstore array correctly', function(done) {
var self = this
var data = { username: 'user', email: ['foo@bar.com'], phones: [{ number: '123456789', type: 'mobile' }, { number: '987654321', type: 'landline' }] }
this.User
.create(data)
.success(function() {
// Check that the hstore fields are the same when retrieving the user
self.User.find({ where: { username: 'user' }})
.success(function(user) {
expect(user.phones).to.deep.equal(data.phones)
done()
})
})
})
it("should read hstore correctly from multiple rows", function(done) { it("should read hstore correctly from multiple rows", function(done) {
var self = this var self = this
......
...@@ -73,6 +73,11 @@ if (dialect.match(/^postgres/)) { ...@@ -73,6 +73,11 @@ if (dialect.match(/^postgres/)) {
done() done()
}) })
it('should handle arrays correctly', function(done) {
expect(hstore.stringify([{ test: 'value' }, { another: 'val' }])).to.deep.equal(['\"test\"=>\"value\"','\"another\"=>\"val\"'])
done()
});
it('should handle nested objects correctly', function(done) { it('should handle nested objects correctly', function(done) {
expect(hstore.stringify({ test: { nested: 'value' } })).to.equal('"test"=>"{\\"nested\\":\\"value\\"}"') expect(hstore.stringify({ test: { nested: 'value' } })).to.equal('"test"=>"{\\"nested\\":\\"value\\"}"')
done() done()
...@@ -91,7 +96,7 @@ if (dialect.match(/^postgres/)) { ...@@ -91,7 +96,7 @@ if (dialect.match(/^postgres/)) {
describe('parse', function() { describe('parse', function() {
it('should handle null objects correctly', function(done) { it('should handle null objects correctly', function(done) {
expect(hstore.parse(null)).to.deep.equal({ }) expect(hstore.parse(null)).to.equal(null)
done() done()
}) })
...@@ -105,6 +110,11 @@ if (dialect.match(/^postgres/)) { ...@@ -105,6 +110,11 @@ if (dialect.match(/^postgres/)) {
done() done()
}) })
it('should handle arrays correctly', function(done) {
expect(hstore.parse('{"\\"test\\"=>\\"value\\"","\\"another\\"=>\\"val\\""}')).to.deep.equal([{ test: 'value' }, { another: 'val' }])
done()
})
it('should handle nested objects correctly', function(done) { it('should handle nested objects correctly', function(done) {
expect(hstore.parse('"test"=>"{\\"nested\\":\\"value\\"}"')).to.deep.equal({ test: { nested: 'value' } }) expect(hstore.parse('"test"=>"{\\"nested\\":\\"value\\"}"')).to.deep.equal({ test: { nested: 'value' } })
done() done()
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!