Skip to content

Commit

Permalink
Merge pull request sequelize#861 from durango/check-type
Browse files Browse the repository at this point in the history
ENUM types will no longer break in Postgres
  • Loading branch information
sdepold committed Aug 30, 2013
2 parents d585c36 + 3034461 commit bbb9518
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 26 deletions.
9 changes: 6 additions & 3 deletions lib/dao-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,18 @@ module.exports = (function() {
}

if (options.force) {
self.drop().success(doQuery).error(function(err) { emitter.emit('error', err) })
self.drop(options).success(doQuery).error(function(err) { emitter.emit('error', err) })
} else {
doQuery()
}
}).run()
}

DAOFactory.prototype.drop = function() {
return this.QueryInterface.dropTable(this.getTableName(this.tableName))
DAOFactory.prototype.drop = function(options) {
// Only Postgres' QueryGenerator.dropTableQuery() will add schema manually
var isPostgres = this.options.dialect === "postgres" || (!!this.daoFactoryManager && this.daoFactoryManager.sequelize.options.dialect === "postgres")
, tableName = isPostgres ? this.tableName : this.getTableName()
return this.QueryInterface.dropTable(tableName, options)
}

DAOFactory.prototype.dropSchema = function(schema) {
Expand Down
51 changes: 45 additions & 6 deletions lib/dialects/postgres/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ module.exports = (function() {

var dataType = this.pgDataTypeMapping(tableName, attr, attributes[attr])
attrStr.push(this.quoteIdentifier(attr) + " " + dataType)

if (attributes[attr].match(/^ENUM\(/)) {
query = this.pgEnum(tableName, attr, attributes[attr]) + query
}
}

var values = {
Expand Down Expand Up @@ -723,9 +719,52 @@ module.exports = (function() {
return this.quoteIdentifier(Utils.removeTicks(this.escape(val), "'"))
},

pgEnum: function (tableName, attr, dataType) {
pgListEnums: function(tableName, attrName, options) {
if (arguments.length === 1) {
options = tableName
tableName = null
}

var enumName = ''

if (!!tableName && !!attrName) {
enumName = ' AND t.typname=' + this.escape("enum_" + tableName + "_" + attrName) + ' '
}

var query = 'SELECT t.typname enum_name, array_agg(e.enumlabel) enum_value FROM pg_type t ' +
'JOIN pg_enum e ON t.oid = e.enumtypid ' +
'JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace ' +
'WHERE n.nspname = \'public\' ' + enumName + ' GROUP BY 1'

return query
},

pgEnum: function (tableName, attr, dataType, options) {
var enumName = this.pgEscapeAndQuote("enum_" + tableName + "_" + attr)
var sql = "CREATE TYPE " + enumName + " AS " + dataType.match(/^ENUM\(.+\)/)[0] + "; "
if (!!options && options.force === true) {
sql = this.pgEnumDrop(tableName, attr) + sql
}
return sql
},

pgEnumAdd: function(tableName, attr, value, options) {
var enumName = this.pgEscapeAndQuote("enum_" + tableName + "_" + attr)
var sql = 'ALTER TYPE ' + enumName + ' ADD VALUE ' + this.escape(value)

if (!!options.before) {
sql += ' BEFORE ' + this.escape(options.before)
}
else if (!!options.after) {
sql += ' AFTER ' + this.escape(options.after)
}

return sql
},

pgEnumDrop: function(tableName, attr) {
var enumName = this.pgEscapeAndQuote("enum_" + tableName + "_" + attr)
return "DROP TYPE IF EXISTS " + enumName + "; CREATE TYPE " + enumName + " AS " + dataType.match(/^ENUM\(.+\)/)[0] + "; "
return 'DROP TYPE IF EXISTS ' + enumName + '; '
},

fromArray: function(text) {
Expand Down
167 changes: 152 additions & 15 deletions lib/query-interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,25 +63,161 @@ module.exports = (function() {
}

QueryInterface.prototype.createTable = function(tableName, attributes, options) {
var attributeHashes = {}

Utils._.each(attributes, function(dataTypeOrOptions, attributeName) {
if (Utils._.values(DataTypes).indexOf(dataTypeOrOptions) > -1) {
attributeHashes[attributeName] = { type: dataTypeOrOptions, allowNull: true }
var attributeHashes = {}
, dataTypeValues = Utils._.values(DataTypes)
, keys = Object.keys(attributes)
, keyLen = keys.length
, self = this
, sql = ''
, i = 0

for (i = 0; i < keyLen; i++) {
if (dataTypeValues.indexOf(attributes[keys[i]]) > -1) {
attributeHashes[keys[i]] = { type: attributes[keys[i]], allowNull: true }
} else {
attributeHashes[attributeName] = dataTypeOrOptions
attributeHashes[keys[i]] = attributes[keys[i]]
}
})
}

return new Utils.CustomEventEmitter(function(emitter) {
// Postgres requires a special SQL command for enums
if (self.sequelize.options.dialect === "postgres") {
var chainer = new Utils.QueryChainer()
// For backwards-compatibility, public schemas don't need to
// explicitly state their schema when creating a new enum type
, getTableName = (!options || !options.schema || options.schema === "public" ? '' : options.schema + '_') + tableName

for (i = 0; i < keyLen; i++) {
if (attributes[keys[i]].toString().match(/^ENUM\(/)) {
sql = self.QueryGenerator.pgListEnums(getTableName, keys[i], options)
chainer.add(self.sequelize.query(sql, null, { plain: true, raw: true, type: 'SELECT' }))
}
}

chainer.runSerially().success(function(results) {
var chainer2 = new Utils.QueryChainer()
// Find the table that we're trying to create throgh DAOFactoryManager
, daoTable = self.sequelize.daoFactoryManager.daos.filter(function(dao) { return dao.tableName === tableName })
, enumIdx = 0

daoTable = daoTable.length > 0 ? daoTable[0] : null

for (i = 0; i < keyLen; i++) {
if (attributes[keys[i]].toString().match(/^ENUM\(/)) {
// If the enum type doesn't exist then create it
if (!results[enumIdx]) {
sql = self.QueryGenerator.pgEnum(getTableName, keys[i], attributes[keys[i]], options)
chainer2.add(self.sequelize.query(sql, null, { raw: true }))
}
else if (!!results[enumIdx] && !!daoTable) {
var enumVals = self.QueryGenerator.fromArray(results[enumIdx].enum_value)
, vals = daoTable.rawAttributes[keys[i]].values

vals.forEach(function(value, idx) {
// reset out after/before options since it's for every enum value
options.before = null
options.after = null

if (enumVals.indexOf(value) === -1) {
if (!!vals[idx+1]) {
options.before = vals[idx+1]
}
else if (!!vals[idx-1]) {
options.after = vals[idx-1]
}

chainer2.add(self.sequelize.query(self.QueryGenerator.pgEnumAdd(getTableName, keys[i], value, options)))
}
})
}
}
}

attributes = this.QueryGenerator.attributesToSQL(attributeHashes)
attributes = self.QueryGenerator.attributesToSQL(attributeHashes)
sql = self.QueryGenerator.createTableQuery(tableName, attributes, options)

var sql = this.QueryGenerator.createTableQuery(tableName, attributes, options)
return queryAndEmit.call(this, sql, 'createTable')
chainer2.run().success(function() {
queryAndEmit.call(self, sql, 'createTable')
.success(function(res) {
self.emit('createTable', null)
emitter.emit('success', res)
})
.error(function(err) {
self.emit('createTable', err)
emitter.emit('error', err)
})
.on('sql', function(sql) { emitter.emit('sql', sql) })
}).error(function(err) {
emitter.emit('error', err)
}).on('sql', function(sql) {
emitter.emit('sql', sql)
})
})
} else {
attributes = self.QueryGenerator.attributesToSQL(attributeHashes)
sql = self.QueryGenerator.createTableQuery(tableName, attributes, options)

queryAndEmit.call(self, sql, 'createTable', emitter).success(function(results) {
self.emit('createTable', null)
emitter.emit('success', results)
}).error(function(err) {
self.emit('createTable', err)
emitter.emit('error', err)
}).on('sql', function(sql) {
emitter.emit('sql', sql)
})
}
}).run()
}

QueryInterface.prototype.dropTable = function(tableName, options) {
var sql = this.QueryGenerator.dropTableQuery(tableName, options)
return queryAndEmit.call(this, sql, 'dropTable')
// if we're forcing we should be cascading unless explicitly stated otherwise
options = options || {}
options.cascade = options.cascade || options.force || false

var sql = this.QueryGenerator.dropTableQuery(tableName, options)
, self = this

return new Utils.CustomEventEmitter(function(emitter) {
var chainer = new Utils.QueryChainer()

chainer.add(self, 'queryAndEmit', [sql])

// Since postgres has a special case for enums, we should drop the related
// enum type within the table and attribute
if (self.sequelize.options.dialect === "postgres") {
// Find the table that we're trying to drop
daoTable = self.sequelize.daoFactoryManager.daos.filter(function(dao) {
return dao.tableName === tableName
})

// Just in case if we're trying to drop a non-existing table
daoTable = daoTable.length > 0 ? daoTable[0] : null
if (!!daoTable) {
var getTableName = (!options || !options.schema || options.schema === "public" ? '' : options.schema + '_') + tableName

var keys = Object.keys(daoTable.rawAttributes)
, keyLen = keys.length
, i = 0

for (i = 0; i < keyLen; i++) {
if (daoTable.rawAttributes[keys[i]].type && daoTable.rawAttributes[keys[i]].type === "ENUM") {
chainer.add(self.sequelize, 'query', [self.QueryGenerator.pgEnumDrop(getTableName, keys[i]), null, {raw: true}])
}
}
}
}

chainer.runSerially().success(function(results) {
emitter.emit('success', results[0])
self.emit('dropTable', null)
}).error(function(err) {
emitter.emit('error', err)
self.emit('dropTable', err)
}).on('sql', function(sql) {
emitter.emit('sql', sql)
})
}).run()
}

QueryInterface.prototype.dropAllTables = function() {
Expand Down Expand Up @@ -156,7 +292,8 @@ module.exports = (function() {
if (self.QueryGenerator.describeTableQuery) {
sql = self.QueryGenerator.describeTableQuery(tableName, schema, schemaDelimiter)
} else {
sql = 'DESCRIBE ' + self.QueryGenerator.addSchema({tableName: tableName, options: {schema: schema, schemaDelimiter: schemaDelimiter}}) + ';'
var table = self.QueryGenerator.quoteIdentifier(self.QueryGenerator.addSchema({tableName: tableName, options: {schema: schema, schemaDelimiter: schemaDelimiter}}), self.QueryGenerator.options.quoteIdentifiers)
sql = 'DESCRIBE ' + table + ';'
}

self.sequelize.query(sql, null, { raw: true }).success(function(data) {
Expand Down Expand Up @@ -361,8 +498,8 @@ module.exports = (function() {
return new Utils.CustomEventEmitter(function(emitter) {
var chainer = new Utils.QueryChainer()

chainer.add(self, 'enableForeignKeyConstraints', [])
chainer.add(self, 'queryAndEmit', [[sql, dao], 'delete'])
chainer.add(self, 'enableForeignKeyConstraints', [])
chainer.add(self, 'queryAndEmit', [[sql, dao], 'delete'])

chainer.runSerially()
.success(function(results){
Expand Down
62 changes: 62 additions & 0 deletions test/postgres/dao.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var chai = require('chai')
, Support = require(__dirname + '/../support')
, dialect = Support.getTestDialect()
, DataTypes = require(__dirname + "/../../lib/data-types")
, _ = require('lodash')

chai.Assertion.includeStack = true

Expand Down Expand Up @@ -32,6 +33,67 @@ if (dialect.match(/^postgres/)) {
})
})

describe('enums', function() {
it('should be able to ignore enum types that already exist', function(done) {
var User = this.sequelize.define('UserEnums', {
mood: DataTypes.ENUM('happy', 'sad', 'meh')
})

User.sync({ force: true }).success(function() {
User.sync().success(function() {
done()
})
})
})

it('should be able to create/drop enums multiple times', function(done) {
var User = this.sequelize.define('UserEnums', {
mood: DataTypes.ENUM('happy', 'sad', 'meh')
})

User.sync({ force: true }).success(function() {
User.sync({ force: true }).success(function() {
done()
})
})
})

it('should be able to add enum types', function(done) {
var self = this
, User = this.sequelize.define('UserEnums', {
mood: DataTypes.ENUM('happy', 'sad', 'meh')
})

var _done = _.after(4, function() {
done()
})

User.sync({ force: true }).success(function() {
User = self.sequelize.define('UserEnums', {
mood: DataTypes.ENUM('neutral', 'happy', 'sad', 'ecstatic', 'meh', 'joyful')
})

User.sync().success(function() {
expect(User.rawAttributes.mood.values).to.deep.equal(['neutral', 'happy', 'sad', 'ecstatic', 'meh', 'joyful'])
_done()
}).on('sql', function(sql) {
if (sql.indexOf('neutral') > -1) {
expect(sql).to.equal("ALTER TYPE \"enum_UserEnums_mood\" ADD VALUE 'neutral' BEFORE 'happy'")
_done()
}
else if (sql.indexOf('ecstatic') > -1) {
expect(sql).to.equal("ALTER TYPE \"enum_UserEnums_mood\" ADD VALUE 'ecstatic' BEFORE 'meh'")
_done()
}
else if (sql.indexOf('joyful') > -1) {
expect(sql).to.equal("ALTER TYPE \"enum_UserEnums_mood\" ADD VALUE 'joyful' AFTER 'meh'")
_done()
}
})
})
})
})

describe('integers', function() {
describe('integer', function() {
beforeEach(function(done) {
Expand Down
4 changes: 2 additions & 2 deletions test/postgres/query-generator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ if (dialect.match(/^postgres/)) {
},
{
arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}],
expectation: "DROP TYPE IF EXISTS \"enum_myTable_title\"; CREATE TYPE \"enum_myTable_title\" AS ENUM(\"A\", \"B\", \"C\"); CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" \"enum_myTable_title\", \"name\" VARCHAR(255));"
expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" \"enum_myTable_title\", \"name\" VARCHAR(255));"
},
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}],
Expand All @@ -163,7 +163,7 @@ if (dialect.match(/^postgres/)) {
},
{
arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}],
expectation: "DROP TYPE IF EXISTS enum_myTable_title; CREATE TYPE enum_myTable_title AS ENUM(\"A\", \"B\", \"C\"); CREATE TABLE IF NOT EXISTS myTable (title enum_myTable_title, name VARCHAR(255));",
expectation: "CREATE TABLE IF NOT EXISTS myTable (title enum_myTable_title, name VARCHAR(255));",
context: {options: {quoteIdentifiers: false}}
},
{
Expand Down

0 comments on commit bbb9518

Please sign in to comment.