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

Commit fce8805a by Sushant Committed by GitHub

add: Support for ARRAY(ENUM) (#8703)

1 parent 13427d49
......@@ -12,5 +12,7 @@ test/sqlite/test.sqlite
coverage-*
site
docs/api/tmp.md
ssce.js
.vscode/
\ No newline at end of file
sscce.js
.vscode/
esdoc
package-lock.json
......@@ -17,6 +17,10 @@ env:
addons:
postgresql: "9.4"
mysql: "5.6"
apt:
packages:
- postgresql-9.4-postgis-2.3
before_script:
- "mysql -e 'create database sequelize_test;'"
......
# Future
- [FIXED] Passing parameters to model getters [#7404](https://github.com/sequelize/sequelize/issues/7404)
- [FIXED] `changeColumn` generates incorrect query with ENUM type [#7456](https://github.com/sequelize/sequelize/pull/7456)
- [ADDED] `ARRAY(ENUM)` support for Postgres
# 3.30.3
- [ADDED] Ability to run transactions on a read-replica by marking transactions as read only [#7323](https://github.com/sequelize/sequelize/issues/7323)
......
......@@ -107,12 +107,14 @@ Sequelize.DECIMAL // DECIMAL
Sequelize.DECIMAL(10, 2) // DECIMAL(10,2)
Sequelize.DATE // DATETIME for mysql / sqlite, TIMESTAMP WITH TIME ZONE for postgres
Sequelize.DATE(6) // DATETIME(6) for mysql 5.6.4+. Fractional seconds support with up to 6 digits of precision
Sequelize.DATE(6) // DATETIME(6) for mysql 5.6.4+. Fractional seconds support with up to 6 digits of precision
Sequelize.DATEONLY // DATE without time.
Sequelize.BOOLEAN // TINYINT(1)
Sequelize.ENUM('value 1', 'value 2') // An ENUM with allowed values 'value 1' and 'value 2'
Sequelize.ARRAY(Sequelize.TEXT) // Defines an array. PostgreSQL only.
Sequelize.ARRAY(Sequelize.ENUM) // Defines an array of enum. PostgreSQL only.
Sequelize.JSON // JSON column. PostgreSQL only.
Sequelize.JSONB // JSONB column. PostgreSQL only.
......
......@@ -1770,7 +1770,7 @@ var QueryGenerator = {
}
}
}
return joinType + this.quoteTable(tableRight, asRight) + ' ON ' + joinOn;
},
......
......@@ -147,9 +147,9 @@ ConnectionManager.prototype.connect = function(config) {
}
}
// oids for hstore and geometry are dynamic - so select them at connection time
// fetch OIDs for Geometry / Hstore / Enum as they are dynamic
if (dataTypes.HSTORE.types.postgres.oids.length === 0) {
query += 'SELECT typname, oid, typarray FROM pg_type WHERE typtype = \'b\' AND typname IN (\'hstore\', \'geometry\', \'geography\')';
query += 'SELECT typname, typtype, oid, typarray FROM pg_type WHERE (typtype = \'b\' AND typname IN (\'hstore\', \'geometry\', \'geography\')) OR (typtype = \'e\')';
}
return new Promise(function (resolve, reject) {
......@@ -161,8 +161,10 @@ ConnectionManager.prototype.connect = function(config) {
type = dataTypes.postgres.GEOMETRY;
} else if (row.typname === 'hstore') {
type = dataTypes.postgres.HSTORE;
} else if (row.typname === 'geography'){
} else if (row.typname === 'geography') {
type = dataTypes.postgres.GEOGRAPHY;
} else if (row.typtype === 'e') {
type = dataTypes.postgres.ENUM;
}
type.types.postgres.oids.push(row.oid);
......
......@@ -394,12 +394,32 @@ module.exports = function (BaseTypes) {
}, this).join(',') + ']';
if (this.type) {
str += '::' + this.toSql();
var Utils = require('../../utils');
var castKey = this.toSql();
if (this.type instanceof BaseTypes.ENUM) {
castKey = Utils.addTicks(
Utils.generateEnumName(options.field.Model.getTableName(), options.field.fieldName),
'"'
) + '[]';
}
str += '::' + castKey;
}
return str;
};
var ENUM = BaseTypes.ENUM.inherits();
ENUM.parse = function (value) {
return value;
};
BaseTypes.ENUM.types.postgres = {
oids: [],
array_oids: []
};
var exports = {
DECIMAL: DECIMAL,
BLOB: BLOB,
......@@ -416,7 +436,8 @@ module.exports = function (BaseTypes) {
GEOMETRY: GEOMETRY,
GEOGRAPHY: GEOGRAPHY,
HSTORE: HSTORE,
RANGE: RANGE
RANGE: RANGE,
ENUM: ENUM
};
_.forIn(exports, function (DataType, key) {
......
......@@ -447,13 +447,25 @@ var QueryGenerator = {
var template = '<%= type %>'
, replacements = {};
if (attribute.type instanceof DataTypes.ENUM) {
if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values;
if (
attribute.type instanceof DataTypes.ENUM ||
(attribute.type instanceof DataTypes.ARRAY && attribute.type.type instanceof DataTypes.ENUM)
) {
var enumType = attribute.type.type || attribute.type;
var values = attribute.values;
if (enumType.values && !attribute.values) {
values = enumType.values;
}
if (Array.isArray(attribute.values) && (attribute.values.length > 0)) {
replacements.type = 'ENUM(' + Utils._.map(attribute.values, function(value) {
if (Array.isArray(values) && (values.length > 0)) {
replacements.type = 'ENUM(' + Utils._.map(values, function(value) {
return this.escape(value);
}.bind(this)).join(', ') + ')';
if (attribute.type instanceof DataTypes.ARRAY) {
replacements.type += '[]';
}
} else {
throw new Error("Values for ENUM haven't been defined.");
}
......@@ -731,8 +743,11 @@ var QueryGenerator = {
pgEnumName: function (tableName, attr, options) {
options = options || {};
var tableDetails = this.extractTableDetails(tableName, options)
, enumName = '"enum_' + tableDetails.tableName + '_' + attr + '"';
var tableDetails = this.extractTableDetails(tableName, options);
var enumName = Utils.addTicks(
Utils.generateEnumName(tableDetails.tableName, attr), '"'
);
// pgListEnums requires the enum name only, without the schema
if (options.schema !== false && tableDetails.schema) {
......@@ -740,7 +755,6 @@ var QueryGenerator = {
}
return enumName;
},
pgListEnums: function(tableName, attrName, options) {
......
......@@ -94,8 +94,14 @@ QueryInterface.prototype.createTable = function(tableName, attributes, options,
var promises = [];
for (i = 0; i < keyLen; i++) {
if (attributes[keys[i]].type instanceof DataTypes.ENUM) {
sql = self.QueryGenerator.pgListEnums(tableName, attributes[keys[i]].field || keys[i], options);
var attribute = attributes[keys[i]];
var type = attribute.type;
if (
type instanceof DataTypes.ENUM ||
(type instanceof DataTypes.ARRAY && type.type instanceof DataTypes.ENUM) //ARRAY sub type is ENUM
) {
sql = self.QueryGenerator.pgListEnums(tableName, attribute.field || keys[i], options);
promises.push(self.sequelize.query(
sql,
_.assign({}, options, { plain: true, raw: true, type: QueryTypes.SELECT })
......@@ -108,17 +114,26 @@ QueryInterface.prototype.createTable = function(tableName, attributes, options,
, enumIdx = 0;
for (i = 0; i < keyLen; i++) {
if (attributes[keys[i]].type instanceof DataTypes.ENUM) {
var attribute = attributes[keys[i]];
var type = attribute.type;
var enumType = type.type || type;
if (
type instanceof DataTypes.ENUM ||
(type instanceof DataTypes.ARRAY && type.type instanceof DataTypes.ENUM) //ARRAY sub type is ENUM
) {
// If the enum type doesn't exist then create it
if (!results[enumIdx]) {
sql = self.QueryGenerator.pgEnum(tableName, attributes[keys[i]].field || keys[i], attributes[keys[i]], options);
sql = self.QueryGenerator.pgEnum(tableName, attribute.field || keys[i], enumType, options);
promises.push(self.sequelize.query(
sql,
_.assign({}, options, { raw: true })
));
} else if (!!results[enumIdx] && !!model) {
var enumVals = self.QueryGenerator.fromArray(results[enumIdx].enum_value)
, vals = model.rawAttributes[keys[i]].values;
, vals = enumType.values;
vals.forEach(function(value, idx) {
// reset out after/before options since it's for every enum value
......@@ -134,7 +149,7 @@ QueryInterface.prototype.createTable = function(tableName, attributes, options,
valueOptions.after = vals[idx - 1];
}
valueOptions.supportsSearchPath = false;
promises.push(self.sequelize.query(self.QueryGenerator.pgEnumAdd(tableName, keys[i], value, valueOptions), valueOptions));
promises.push(self.sequelize.query(self.QueryGenerator.pgEnumAdd(tableName, attribute.field || keys[i], value, valueOptions), valueOptions));
}
});
enumIdx++;
......@@ -153,11 +168,36 @@ QueryInterface.prototype.createTable = function(tableName, attributes, options,
attributes = self.QueryGenerator.attributesToSQL(attributes, {
context: 'createTable'
});
sql = self.QueryGenerator.createTableQuery(tableName, attributes, options);
return Promise.all(promises).then(function() {
return self.sequelize.query(sql, options);
});
return Promise.all(promises)
.then(function() {
// if no enum was processed, then return
if (!promises.length) return;
// fetch OIDs ENUM, refresh them
return self.sequelize.query(
'SELECT typname, typtype, oid, typarray FROM pg_type WHERE typtype = \'e\';',
_.assign({}, options, { raw: true, type: QueryTypes.SELECT })
).then(function (results) {
var dataType = DataTypes.postgres.ENUM;
dataType.types.postgres.oids = [];
dataType.types.postgres.array_oids = [];
results.forEach(function (row) {
dataType.types.postgres.oids.push(row.oid);
dataType.types.postgres.array_oids.push(row.typarray);
});
self.sequelize.dialect.connectionManager.$refreshTypeParser(dataType);
self.sequelize.refreshTypes(DataTypes.postgres);
});
})
.then(function() {
return self.sequelize.query(sql, options);
});
});
} else {
if (!tableName.schema &&
......
......@@ -461,6 +461,10 @@ var Utils = module.exports = {
}
return obj;
},
generateEnumName: function (tableName, attributeName) {
return 'enum_' + tableName + '_' + attributeName;
}
};
......
......@@ -269,42 +269,13 @@ describe(Support.getTestDialectTeaser('DataTypes'), function() {
it('calls parse and stringify for ENUM', function () {
var Type = new Sequelize.ENUM('hat', 'cat');
// No dialects actually allow us to identify that we get an enum back..
testFailure(Type);
if (['postgres'].indexOf(dialect) !== -1) {
return testSuccess(Type, 'hat');
} else {
testFailure(Type);
}
});
it('should parse an empty GEOMETRY field', function () {
var Type = new Sequelize.GEOMETRY();
if (current.dialect.supports.GEOMETRY) {
current.refreshTypes();
var User = current.define('user', { field: Type }, { timestamps: false });
var point = { type: "Point", coordinates: [] };
return current.sync({ force: true }).then(function () {
return User.create({
//insert a null GEOMETRY type
field: point
});
}).then(function () {
//This case throw unhandled exception
return User.findAll();
}).then(function(users){
if (Support.dialectIsMySQL()) {
// MySQL will return NULL, becuase they lack EMPTY geometry data support.
expect(users[0].field).to.be.eql(null);
} else if (dialect === 'postgres' || dialect === 'postgres-native') {
//Empty Geometry data [0,0] as per https://trac.osgeo.org/postgis/ticket/1996
expect(users[0].field).to.be.deep.eql({ type: "Point", coordinates: [0,0] });
} else {
expect(users[0].field).to.be.deep.eql(point);
}
});
}
});
if (dialect === 'postgres' || dialect === 'sqlite') {
// postgres actively supports IEEE floating point literals, and sqlite doesn't care what we throw at it
it('should store and parse IEEE floating point literals (NaN and Infinity)', function () {
......
......@@ -393,6 +393,165 @@ if (dialect.match(/^postgres/)) {
expect(enums[0].enum_value).to.equal("{neutral,happy,sad,ecstatic,meh,joyful}");
});
});
describe('ARRAY(ENUM)', function () {
it('should be able to ignore enum types that already exist', function() {
var User = this.sequelize.define('UserEnums', {
permissions: DataTypes.ARRAY(DataTypes.ENUM([
'access',
'write',
'check',
'delete'
]))
});
return User.sync({ force: true }).then(function() {
return User.sync();
});
});
it('should be able to create/drop enums multiple times', function() {
var User = this.sequelize.define('UserEnums', {
permissions: DataTypes.ARRAY(DataTypes.ENUM([
'access',
'write',
'check',
'delete'
]))
});
return User.sync({ force: true }).then(function() {
return User.sync({ force: true });
});
});
it('should be able to add values to enum types', function() {
var User = this.sequelize.define('UserEnums', {
permissions: DataTypes.ARRAY(DataTypes.ENUM([
'access',
'write',
'check',
'delete'
]))
});
return User.sync({ force: true }).bind(this).then(function() {
User = this.sequelize.define('UserEnums', {
permissions: DataTypes.ARRAY(
DataTypes.ENUM('view', 'access', 'edit', 'write', 'check', 'delete')
)
});
return User.sync();
}).then(function() {
return this.sequelize.getQueryInterface().pgListEnums(User.getTableName());
}).then(function (enums) {
expect(enums).to.have.length(1);
expect(enums[0].enum_value).to.equal("{view,access,edit,write,check,delete}");
});
});
it('should be able to insert new record', function() {
var User = this.sequelize.define('UserEnums', {
name: DataTypes.STRING,
type: DataTypes.ENUM('A', 'B', 'C'),
owners: DataTypes.ARRAY(DataTypes.STRING),
permissions: DataTypes.ARRAY(DataTypes.ENUM([
'access',
'write',
'check',
'delete'
]))
});
return User.sync({ force: true })
.then(function() {
return User.create({
name: 'file.exe',
type: 'C',
owners: ['userA', 'userB'],
permissions: ['access', 'write']
});
})
.then(function (user) {
expect(user.name).to.equal('file.exe');
expect(user.type).to.equal('C');
expect(user.owners).to.deep.equal(['userA', 'userB']);
expect(user.permissions).to.deep.equal(['access', 'write']);
});
});
it('should fail when trying to insert foreign element on ARRAY(ENUM)', function() {
var User = this.sequelize.define('UserEnums', {
name: DataTypes.STRING,
type: DataTypes.ENUM('A', 'B', 'C'),
owners: DataTypes.ARRAY(DataTypes.STRING),
permissions: DataTypes.ARRAY(DataTypes.ENUM([
'access',
'write',
'check',
'delete'
]))
});
return expect(User.sync({ force: true }).then(function() {
return User.create({
name: 'file.exe',
type: 'C',
owners: ['userA', 'userB'],
permissions: ['cosmic_ray_disk_access']
});
})).to.be.rejectedWith(/invalid input value for enum "enum_UserEnums_permissions": "cosmic_ray_disk_access"/);
});
it('should be able to find records', function() {
var User = this.sequelize.define('UserEnums', {
name: DataTypes.STRING,
type: DataTypes.ENUM('A', 'B', 'C'),
permissions: DataTypes.ARRAY(DataTypes.ENUM([
'access',
'write',
'check',
'delete'
]))
});
return User.sync({ force: true })
.then(function() {
return User.bulkCreate([{
name: 'file1.exe',
type: 'C',
permissions: ['access', 'write']
}, {
name: 'file2.exe',
type: 'A',
permissions: ['access', 'check']
}, {
name: 'file3.exe',
type: 'B',
permissions: ['access', 'write', 'delete']
}]);
})
.then(function() {
return User.findAll({
where: {
type: {
$in: ['A', 'C']
},
permissions: {
$contains: ['write']
}
}
});
})
.then(function(users) {
expect(users.length).to.equal(1);
expect(users[0].name).to.equal('file1.exe');
expect(users[0].type).to.equal('C');
expect(users[0].permissions).to.deep.equal(['access', 'write']);
});
});
});
});
describe('integers', function() {
......
......@@ -53,7 +53,9 @@ describe(Support.getTestDialectTeaser('Model'), function() {
, where = {prop: Math.random().toString()}
, findSpy = this.sinon.stub(Model, 'findOne');
this.sinon.stub(Model, 'create').returns(Promise.reject(new UniqueConstraintError()));
this.sinon.stub(Model, 'create', function() {
return Promise.reject(new UniqueConstraintError());
});
findSpy.onFirstCall().returns(Promise.resolve(null));
findSpy.onSecondCall().returns(Promise.resolve(result));
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!