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

Commit ffc50e13 by Jan Aagaard Meier

Merge pull request #2295 from seth-admittedly/feature/hstore-test

Use pg-hstore for improved hstore handling
2 parents 4b55fa21 9e776400
......@@ -6,6 +6,7 @@
#### Backwards compatability changes
- When eager-loading a many-to-many association, the attributes of the through table are now accessible through an attribute named after the through model rather than the through table name singularized. i.e. `Task.find({include: Worker})` where the table name for through model `TaskWorker` is `TableTaskWorkers` used to produce `{ Worker: { ..., TableTaskWorker: {...} } }`. It now produces `{ Worker: { ..., TaskWorker: {...} } }`. Does not affect models where table name is auto-defined by Sequelize, or where table name is model name pluralized.
- When using `Model#find()` with an `order` clause, the table name is prepended to the `ORDER BY` SQL. e.g. `ORDER BY Task.id` rather than `ORDER BY id`. The change is to avoid ambiguous column names where there are eager-loaded associations with the same column names. A side effect is that code like `Task.findAll( { include: [ User ], order: [ [ 'Users.id', 'ASC' ] ] } )` will now throw an error. This should be achieved with `Task.findAll( { include: [ User ], order: [ [ User, 'id', 'ASC' ] ] } )` instead.
- Nested HSTORE objects are no longer supported. Use DataTypes.JSON instead.
# 2.0.0-rc2
- [FEATURE] Added to posibility of using a sequelize object as key in `sequelize.where`. Also added the option of specifying a comparator
......
'use strict';
module.exports = {
stringifyPart: function(part) {
switch (typeof part) {
case 'boolean':
case 'number':
return String(part);
case 'string':
return '"' + part.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
case 'undefined':
return 'NULL';
default:
if (part === null)
return 'NULL';
else
return '"' + JSON.stringify(part).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
}
},
stringifyObject: function(data) {
var self = this;
var hstore = require("pg-hstore")({sanitize : true});
return Object.keys(data).map(function(key) {
return self.stringifyPart(key) + '=>' + self.stringifyPart(data[key]);
}).join(',');
},
stringifyArray: function(data) {
return data.map(this.stringifyObject, this);
},
module.exports = {
stringify: function(data) {
if (Array.isArray(data)) {
return this.stringifyArray(data);
}
return this.stringifyObject(data);
},
parsePart: function(part) {
part = part.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
switch (part[0]) {
case '{':
case '[':
return JSON.parse(part);
default:
return part;
}
},
parseObject: function(string) {
var self = this,
object = { };
if (0 === string.length) {
return object;
}
var rx = /\"((?:\\\"|[^"])*)\"\s*\=\>\s*((?:true|false|NULL|\d+|\d+\.\d+|\"((?:\\\"|[^"])*)\"))/g;
string = string || '';
if(data === null) return null;
string.replace(rx, function(match, key, value, innerValue) {
switch (value) {
case 'true':
object[self.parsePart(key)] = true;
break;
case 'false':
object[self.parsePart(key)] = false;
break;
case 'NULL':
object[self.parsePart(key)] = null;
break;
default:
object[self.parsePart(key)] = self.parsePart(innerValue || value);
break;
}
});
return object;
},
parseArray: function(string) {
var matches = string.match(/{(.*)}/);
var array = JSON.parse('['+ matches[1] +']');
return array.map(this.parseObject, this);
return hstore.stringify(data);
},
parse: function(value) {
if ('string' !== typeof value) {
return value;
}
if ('{' === value[0] && '}' === value[value.length - 1]) {
return this.parseArray(value);
}
if(value === null) return null;
return this.parseObject(value);
return hstore.parse(value);
}
};
......@@ -852,7 +852,11 @@ module.exports = (function() {
}
if (Utils._.isObject(value) && field && (field.type === DataTypes.HSTORE || field.type === DataTypes.ARRAY(DataTypes.HSTORE))) {
value = hstore.stringify(value);
if(field.type === DataTypes.HSTORE){
return "'" + hstore.stringify(value) + "'";
}else if (field.type === DataTypes.ARRAY(DataTypes.HSTORE)){
return "ARRAY[" + Utils._.map(value, function(v){return "'" + hstore.stringify(v) + "'::hstore";}).join(",") + "]::HSTORE[]";
}
} else if (Utils._.isObject(value) && field && (field.type === DataTypes.JSON)) {
value = JSON.stringify(value);
}
......
......@@ -12,13 +12,16 @@ var Utils = require('../../utils')
// This cannot be done in the 'pg' lib because hstore is a UDT.
var parseHstoreFields = function(model, row) {
Utils._.forEach(row, function(value, key) {
if (model._isHstoreAttribute(key) || (model.attributes[key] && model.attributes[key].type === DataTypes.ARRAY(DataTypes.HSTORE))) {
row[key] = hstore.parse(value);
if(value === null) return row[key] = null;
return;
if (model._isHstoreAttribute(key)) {
row[key] = hstore.parse(value);
}else if(model.attributes[key] && model.attributes[key].type === DataTypes.ARRAY(DataTypes.HSTORE)) {
var array = JSON.parse('[' + value.slice(1).slice(0,-1) + ']');
row[key] = Utils._.map(array, function(v){return hstore.parse(v);});
}else{
row[key] = value;
}
row[key] = value;
});
};
......
......@@ -40,6 +40,7 @@
"node-uuid": "~1.4.1",
"bluebird": "~2.3.2",
"sql": "~0.40.0",
"pg-hstore": "^2.2.0",
"toposort-class": "~0.3.0",
"validator": "~3.22.0"
},
......
......@@ -16,7 +16,7 @@ if (dialect.match(/^postgres/)) {
username: DataTypes.STRING,
email: { type: DataTypes.ARRAY(DataTypes.TEXT) },
settings: DataTypes.HSTORE,
document: { type: DataTypes.HSTORE, defaultValue: { default: 'value' } },
document: { type: DataTypes.HSTORE, defaultValue: { default: "'value'" } },
phones: DataTypes.ARRAY(DataTypes.HSTORE),
emergency_contact: DataTypes.JSON
})
......@@ -66,7 +66,7 @@ if (dialect.match(/^postgres/)) {
username: 'bob',
emergency_contact: { name: 'joe', phones: [1337, 42] }
}).on('sql', function (sql) {
var expected = 'INSERT INTO "Users" ("id","username","document","emergency_contact","createdAt","updatedAt") VALUES (DEFAULT,\'bob\',\'"default"=>"value"\',\'{"name":"joe","phones":[1337,42]}\''
var expected = 'INSERT INTO "Users" ("id","username","document","emergency_contact","createdAt","updatedAt") VALUES (DEFAULT,\'bob\',\'"default"=>"\'\'value\'\'"\',\'{"name":"joe","phones":[1337,42]}\''
expect(sql.indexOf(expected)).to.equal(0);
});
});
......@@ -179,25 +179,24 @@ if (dialect.match(/^postgres/)) {
});
describe('hstore', function() {
it('should tell me that a column is hstore and not USER-DEFINED', function(done) {
this.sequelize.queryInterface.describeTable('Users').success(function(table) {
it('should tell me that a column is hstore and not USER-DEFINED', function() {
return this.sequelize.queryInterface.describeTable('Users').then(function(table) {
expect(table.settings.type).to.equal('HSTORE')
expect(table.document.type).to.equal('HSTORE')
done()
})
})
it('should stringify hstore with insert', function(done) {
this.User.create({
it('should stringify hstore with insert', function() {
return this.User.create({
username: 'bob',
email: ['myemail@email.com'],
settings: {mailing: false, push: 'facebook', frequency: 3}
}).on('sql', function(sql) {
var expected = 'INSERT INTO "Users" ("id","username","email","settings","document","createdAt","updatedAt") VALUES (DEFAULT,\'bob\',ARRAY[\'myemail@email.com\']::TEXT[],\'"mailing"=>false,"push"=>"facebook","frequency"=>3\',\'"default"=>"value"\''
var expected = 'INSERT INTO "Users" ("id","username","email","settings","document","createdAt","updatedAt") VALUES (DEFAULT,\'bob\',ARRAY[\'myemail@email.com\']::TEXT[],\'"mailing"=>"false","push"=>"facebook","frequency"=>"3"\',\'"default"=>"\'\'value\'\'"\''
expect(sql.indexOf(expected)).to.equal(0)
done()
})
})
})
describe('enums', function() {
......@@ -403,106 +402,105 @@ if (dialect.match(/^postgres/)) {
})
})
it("should save hstore correctly", function(done) {
this.User
.create({ username: 'user', email: ['foo@bar.com'], settings: { created: { test: '"value"' }}})
.success(function(newUser) {
it("should save hstore correctly", function() {
return this.User.create({ username: 'user', email: ['foo@bar.com'], settings: { created: '"value"' }}).then(function(newUser) {
// Check to see if the default value for an hstore field works
expect(newUser.document).to.deep.equal({ default: 'value' })
expect(newUser.settings).to.deep.equal({ created: { test: '"value"' }})
expect(newUser.document).to.deep.equal({ default: "'value'" })
expect(newUser.settings).to.deep.equal({ created: '"value"' })
// Check to see if updating an hstore field works
newUser.updateAttributes({settings: {should: 'update', to: 'this', first: 'place'}}).success(function(oldUser){
return newUser.updateAttributes({settings: {should: 'update', to: 'this', first: 'place'}}).then(function(oldUser){
// Postgres always returns keys in alphabetical order (ascending)
expect(oldUser.settings).to.deep.equal({first: 'place', should: 'update', to: 'this'})
done()
})
})
.error(console.log)
})
it('should save hstore array correctly', function(done) {
this.User.create({
it('should save hstore array correctly', function() {
var User = this.User;
return 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()
phones: [{ number: '123456789', type: 'mobile' }, { number: '987654321', type: 'landline' }, { number : '8675309', type : "Jenny's"}, {number : '5555554321', type : '"home"' }]
}).then(function(){
return User.find(1).then(function(user){
expect(user.phones.length).to.equal(4);
expect(user.phones[1].number).to.equal('987654321');
expect(user.phones[2].type).to.equal("Jenny's");
expect(user.phones[3].type).to.equal('"home"');
})
})
})
it("should update hstore correctly", function(done) {
var self = this
it('should bulkCreate with hstore property', function() {
var User = this.User;
this.User
.create({ username: 'user', email: ['foo@bar.com'], settings: { created: { test: '"value"' }}})
.success(function(newUser) {
return this.User.bulkCreate([{
username: 'bob',
email: ['myemail@email.com'],
settings: {mailing: true, push: 'facebook', frequency: 3}
}]).then(function(){
return User.find(1).then(function(user){
expect(user.settings.mailing).to.equal("true")
})
})
})
it("should update hstore correctly", function() {
var self = this;
return this.User.create({ username: 'user', email: ['foo@bar.com'], settings: { test: '"value"' }}).then(function(newUser) {
// Check to see if the default value for an hstore field works
expect(newUser.document).to.deep.equal({default: 'value'})
expect(newUser.settings).to.deep.equal({ created: { test: '"value"' }})
expect(newUser.document).to.deep.equal({default: "'value'"})
expect(newUser.settings).to.deep.equal({ test: '"value"' })
// Check to see if updating an hstore field works
self.User.update({settings: {should: 'update', to: 'this', first: 'place'}}, {where: newUser.identifiers}).success(function() {
newUser.reload().success(function() {
return self.User.update({settings: {should: 'update', to: 'this', first: 'place'}}, {where: newUser.identifiers}).then(function() {
return newUser.reload().success(function() {
// Postgres always returns keys in alphabetical order (ascending)
expect(newUser.settings).to.deep.equal({first: 'place', should: 'update', to: 'this'})
done()
});
})
})
})
.error(console.log)
})
it("should update hstore correctly and return the affected rows", function(done) {
it("should update hstore correctly and return the affected rows", function() {
var self = this
this.User
.create({ username: 'user', email: ['foo@bar.com'], settings: { created: { test: '"value"' }}})
.success(function(oldUser) {
return this.User.create({ username: 'user', email: ['foo@bar.com'], settings: { test: '"value"' }}).then(function(oldUser) {
// Update the user and check that the returned object's fields have been parsed by the hstore library
self.User.update({settings: {should: 'update', to: 'this', first: 'place'}}, {where: oldUser.identifiers, returning: true }).spread(function(count, users) {
return self.User.update({settings: {should: 'update', to: 'this', first: 'place'}}, {where: oldUser.identifiers, returning: true }).spread(function(count, users) {
expect(count).to.equal(1);
expect(users[0].settings).to.deep.equal({should: 'update', to: 'this', first: 'place'})
done()
})
})
.error(console.log)
})
it("should read hstore correctly", function(done) {
it("should read hstore correctly", function() {
var self = this
var data = { username: 'user', email: ['foo@bar.com'], settings: { created: { test: '"value"' }}}
var data = { username: 'user', email: ['foo@bar.com'], settings: { test: '"value"' }}
this.User
.create(data)
.success(function() {
return this.User.create(data)
.then(function() {
return self.User.find({ where: { username: 'user' }})
})
.then(function(user){
// Check that the hstore fields are the same when retrieving the user
self.User.find({ where: { username: 'user' }})
.success(function(user) {
expect(user.settings).to.deep.equal(data.settings)
done()
})
expect(user.settings).to.deep.equal(data.settings)
})
.error(console.log)
})
it('should read an hstore array correctly', function(done) {
it('should read an hstore array correctly', function() {
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() {
return this.User.create(data)
.then(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()
})
return self.User.find({ where: { username: 'user' }});
}).then(function(user) {
expect(user.phones).to.deep.equal(data.phones)
})
})
......@@ -510,24 +508,24 @@ if (dialect.match(/^postgres/)) {
var self = this
self.User
.create({ username: 'user1', email: ['foo@bar.com'], settings: { created: { test: '"value"' }}})
.create({ username: 'user1', email: ['foo@bar.com'], settings: { test: '"value"' }})
.then(function() {
return self.User.create({ username: 'user2', email: ['foo2@bar.com'], settings: { updated: { another: '"example"' }}})
return self.User.create({ username: 'user2', email: ['foo2@bar.com'], settings: { another : '"example"' }})
})
.then(function() {
// Check that the hstore fields are the same when retrieving the user
return self.User.findAll({ order: 'username' })
})
.then(function(users) {
expect(users[0].settings).to.deep.equal({ created: { test: '"value"' }})
expect(users[1].settings).to.deep.equal({ updated: { another: '"example"' }})
expect(users[0].settings).to.deep.equal({ test: '"value"' })
expect(users[1].settings).to.deep.equal({ another: '"example"' })
done()
})
.error(console.log)
})
})
describe('[POSTGRES] Unquoted identifiers', function() {
it("can insert and select", function(done) {
var self = this
......
......@@ -3,131 +3,83 @@ var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, dialect = Support.getTestDialect()
, hstore = require(__dirname + '/../../lib/dialects/postgres/hstore')
, hstore = require("../../lib/dialects/postgres/hstore")
chai.config.includeStack = true
if (dialect.match(/^postgres/)) {
describe('[POSTGRES Specific] hstore', function() {
describe('stringifyPart', function() {
it("handles undefined values correctly", function(done) {
expect(hstore.stringifyPart(undefined)).to.equal('NULL')
done()
describe('stringify', function() {
it('should handle empty objects correctly', function() {
expect(hstore.stringify({ })).to.equal('')
})
it("handles null values correctly", function(done) {
expect(hstore.stringifyPart(null)).to.equal('NULL')
done()
it('should handle null values correctly', function() {
expect(hstore.stringify({ null: null })).to.equal('"null"=>NULL')
})
it("handles boolean values correctly", function(done) {
expect(hstore.stringifyPart(false)).to.equal('false')
expect(hstore.stringifyPart(true)).to.equal('true')
done()
it('should handle null values correctly', function() {
expect(hstore.stringify({ foo: null })).to.equal('"foo"=>NULL')
})
it("handles strings correctly", function(done) {
expect(hstore.stringifyPart('foo')).to.equal('"foo"')
it('should handle empty string correctly', function(done) {
expect(hstore.stringify({foo : ""})).to.equal('"foo"=>\"\"')
done()
})
it("handles strings with backslashes correctly", function(done) {
expect(hstore.stringifyPart("\\'literally\\'")).to.equal('"\\\\\'literally\\\\\'"')
done()
it('should handle a string with backslashes correctly', function() {
expect(hstore.stringify({foo : "\\"})).to.equal('"foo"=>"\\\\"')
})
it("handles arrays correctly", function(done) {
expect(hstore.stringifyPart([1,['2'],'"3"'])).to.equal('"[1,[\\"2\\"],\\"\\\\\\"3\\\\\\"\\"]"')
done()
it('should handle a string with double quotes correctly', function() {
expect(hstore.stringify({foo : '""a"'})).to.equal('"foo"=>"\\"\\"a\\""')
})
it("handles simple objects correctly", function(done) {
expect(hstore.stringifyPart({ test: 'value' })).to.equal('"{\\"test\\":\\"value\\"}"')
done()
it('should handle a string with single quotes correctly', function() {
expect(hstore.stringify({foo : "''a'"})).to.equal('"foo"=>"\'\'\'\'a\'\'"')
})
it("handles nested objects correctly", function(done) {
expect(hstore.stringifyPart({ test: { nested: 'value' } })).to.equal('"{\\"test\\":{\\"nested\\":\\"value\\"}}"')
done()
it('should handle simple objects correctly', function() {
expect(hstore.stringify({ test: 'value' })).to.equal('"test"=>"value"')
})
it("handles objects correctly", function(done) {
expect(hstore.stringifyPart({test: {nested: {value: {including: '"string"'}}}})).to.equal('"{\\"test\\":{\\"nested\\":{\\"value\\":{\\"including\\":\\"\\\\\\"string\\\\\\"\\"}}}}"')
done()
})
})
describe('stringify', function() {
it('should handle empty objects correctly', function(done) {
expect(hstore.stringify({ })).to.equal('')
done()
})
it('should handle null values correctly', function(done) {
expect(hstore.stringify({ null: null })).to.equal('"null"=>NULL')
done()
})
it('should handle simple objects correctly', function(done) {
expect(hstore.stringify({ test: 'value' })).to.equal('"test"=>"value"')
done()
describe('parse', function() {
it('should handle a null object correctly', function() {
expect(hstore.parse(null)).to.deep.equal(null)
})
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) {
expect(hstore.stringify({ test: { nested: 'value' } })).to.equal('"test"=>"{\\"nested\\":\\"value\\"}"')
done()
it('should handle empty string correctly', function() {
expect(hstore.parse('"foo"=>\"\"')).to.deep.equal({foo : ""})
})
it('should handle nested arrays correctly', function(done) {
expect(hstore.stringify({ test: [ 1, '2', [ '"3"' ] ] })).to.equal('"test"=>"[1,\\"2\\",[\\"\\\\\\"3\\\\\\"\\"]]"')
done()
it('should handle a string with double quotes correctly', function() {
expect(hstore.parse('"foo"=>"\\\"\\\"a\\\""')).to.deep.equal({foo : "\"\"a\""})
})
it('should handle multiple keys with different types of values', function(done) {
expect(hstore.stringify({ true: true, false: false, null: null, undefined: undefined, integer: 1, array: [1,'2'], object: { object: 'value' }})).to.equal('"true"=>true,"false"=>false,"null"=>NULL,"undefined"=>NULL,"integer"=>1,"array"=>"[1,\\"2\\"]","object"=>"{\\"object\\":\\"value\\"}"')
done()
it('should handle a string with single quotes correctly', function() {
expect(hstore.parse('"foo"=>"\'\'\'\'a\'\'"')).to.deep.equal({foo : "''a'"})
})
})
describe('parse', function() {
it('should handle null objects correctly', function(done) {
expect(hstore.parse(null)).to.equal(null)
done()
it('should handle a string with backslashes correctly', function() {
expect(hstore.parse('"foo"=>"\\\\"')).to.deep.equal({foo : "\\"})
})
it('should handle empty objects correctly', function(done) {
it('should handle empty objects correctly', function() {
expect(hstore.parse('')).to.deep.equal({ })
done()
})
it('should handle simple objects correctly', function(done) {
it('should handle simple objects correctly', function() {
expect(hstore.parse('"test"=>"value"')).to.deep.equal({ test: 'value' })
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) {
expect(hstore.parse('"test"=>"{\\"nested\\":\\"value\\"}"')).to.deep.equal({ test: { nested: 'value' } })
done()
})
it('should handle nested arrays correctly', function(done) {
expect(hstore.parse('"test"=>"[1,\\"2\\",[\\"\\\\\\"3\\\\\\"\\"]]"')).to.deep.equal({ test: [ 1, '2', [ '"3"' ] ] })
done()
})
it('should handle multiple keys with different types of values', function(done) {
expect(hstore.parse('"true"=>true,"false"=>false,"null"=>NULL,"undefined"=>NULL,"integer"=>1,"array"=>"[1,\\"2\\"]","object"=>"{\\"object\\":\\"value\\"}"')).to.deep.equal({ true: true, false: false, null: null, undefined: null, integer: "1", array: [1,'2'], object: { object: 'value' }})
done()
})
describe('stringify and parse', function() {
it('should stringify then parse back the same structure', function(){
var testObj = {foo : "bar", count : "1", emptyString : "", quotyString : '""', extraQuotyString : '"""a"""""', backslashes : '\\f023', moreBackslashes : '\\f\\0\\2\\1', backslashesAndQuotes : '\\"\\"uhoh"\\"', nully : null};
expect(hstore.parse(hstore.stringify(testObj))).to.deep.equal(testObj);
expect(hstore.parse(hstore.stringify(hstore.parse(hstore.stringify(testObj))))).to.deep.equal(testObj);
})
})
})
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!