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

Commit 3d2df28e by Carson McManus Committed by GitHub

fix(sqlite): describeTable now returns unique and references (#12440)

1 parent 56d07c6d
......@@ -434,7 +434,8 @@ class SQLiteQueryGenerator extends MySqlQueryGenerator {
).join(', ');
const attributeNamesExport = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).join(', ');
return `${this.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE')
// Temporary tables don't support foreign keys, so creating a temporary table will not allow foreign keys to be preserved
return `${this.createTableQuery(backupTableName, attributes)
}INSERT INTO ${quotedBackupTableName} SELECT ${attributeNamesImport} FROM ${quotedTableName};`
+ `DROP TABLE ${quotedTableName};${
this.createTableQuery(tableName, attributes)
......
......@@ -60,7 +60,7 @@ function changeColumn(qi, tableName, attributes, options) {
options = options || {};
return qi.describeTable(tableName, options).then(fields => {
fields[attributeName] = attributes[attributeName];
Object.assign(fields[attributeName], attributes[attributeName]);
const sql = qi.QueryGenerator.removeColumnQuery(tableName, fields);
const subQueries = sql.split(';').filter(q => q !== '');
......@@ -206,3 +206,94 @@ function getForeignKeyReferencesForTable(qi, tableName, options) {
}
exports.getForeignKeyReferencesForTable = getForeignKeyReferencesForTable;
/**
* Describe a table structure
*
* This method returns an array of hashes containing information about all attributes in the table.
*
* ```js
* {
* name: {
* type: 'VARCHAR(255)', // this will be 'CHARACTER VARYING' for pg!
* allowNull: true,
* defaultValue: null,
* unique: true, // available for sqlite only
* references: {}, // available for sqlite only
* },
* isBetaMember: {
* type: 'TINYINT(1)', // this will be 'BOOLEAN' for pg!
* allowNull: false,
* defaultValue: false,
* unique: false, // available for sqlite only
* references: {}, // available for sqlite only
* }
* }
* ```
*
* @param {QueryInterface} qi
* @param {string} tableName table name
* @param {Object} [options] Query options
*
* @returns {Promise<Object>}
*/
function describeTable(qi, tableName, options) {
let schema = null;
let schemaDelimiter = null;
if (typeof options === 'string') {
schema = options;
} else if (typeof options === 'object' && options !== null) {
schema = options.schema || null;
schemaDelimiter = options.schemaDelimiter || null;
}
if (typeof tableName === 'object' && tableName !== null) {
schema = tableName.schema;
tableName = tableName.tableName;
}
const sql = qi.QueryGenerator.describeTableQuery(tableName, schema, schemaDelimiter);
options = Object.assign({}, options, { type: QueryTypes.DESCRIBE });
return qi.sequelize.query(sql, options).then(data => {
/*
* If no data is returned from the query, then the table name may be wrong.
* Query generators that use information_schema for retrieving table info will just return an empty result set,
* it will not throw an error like built-ins do (e.g. DESCRIBE on MySql).
*/
if (_.isEmpty(data)) {
throw new Error(`No description found for "${tableName}" table. Check the table name and schema; remember, they _are_ case sensitive.`);
}
return qi.showIndex(tableName, options).then(indexes => {
for (const prop in data) {
data[prop].unique = false;
}
for (const index of indexes) {
for (const field of index.fields) {
if (index.unique !== undefined) {
data[field.attribute].unique = index.unique;
}
}
}
return qi.getForeignKeyReferencesForTable(tableName, options).then(foreignKeys => {
for (const foreignKey of foreignKeys) {
data[foreignKey.columnName].references = {
model: foreignKey.referencedTableName,
key: foreignKey.referencedColumnName
};
}
return data;
});
});
}).catch(e => {
if (e.original && e.original.code === 'ER_NO_SUCH_TABLE') {
throw Error(`No description found for "${tableName}" table. Check the table name and schema; remember, they _are_ case sensitive.`);
}
throw e;
});
}
exports.describeTable = describeTable;
......@@ -450,6 +450,11 @@ class QueryInterface {
* @returns {Promise<Object>}
*/
describeTable(tableName, options) {
if (this.sequelize.options.dialect === 'sqlite') {
// sqlite needs some special treatment as it cannot rename a column
return SQLiteQueryInterface.describeTable(this, tableName, options);
}
let schema = null;
let schemaDelimiter = null;
......
......@@ -2376,7 +2376,11 @@ describe(Support.getTestDialectTeaser('Model'), () => {
return UserPublic.schema('special').sync({ force: true }).then(() => {
return this.sequelize.queryInterface.describeTable('Publics', {
logging(sql) {
if (dialect === 'sqlite' || dialect === 'mysql' || dialect === 'mssql' || dialect === 'mariadb') {
if (dialect === 'sqlite' && sql.includes('TABLE_INFO')) {
count++;
expect(sql).to.not.contain('special');
}
else if (['mysql', 'mssql', 'mariadb'].includes(dialect)) {
expect(sql).to.not.contain('special');
count++;
}
......@@ -2389,7 +2393,11 @@ describe(Support.getTestDialectTeaser('Model'), () => {
return this.sequelize.queryInterface.describeTable('Publics', {
schema: 'special',
logging(sql) {
if (dialect === 'sqlite' || dialect === 'mysql' || dialect === 'mssql' || dialect === 'mariadb') {
if (dialect === 'sqlite' && sql.includes('TABLE_INFO')) {
count++;
expect(sql).to.contain('special');
}
else if (['mysql', 'mssql', 'mariadb'].includes(dialect)) {
expect(sql).to.contain('special');
count++;
}
......
......@@ -184,12 +184,16 @@ describe(Support.getTestDialectTeaser('Model'), () => {
.then(results => {
if (dialect === 'sqlite') {
// SQLite doesn't treat primary key as index
expect(results).to.have.length(4);
// However it does create an extra "autoindex", except primary == false
expect(results).to.have.length(4 + 1);
} else {
expect(results).to.have.length(4 + 1);
expect(results.filter(r => r.primary)).to.have.length(1);
}
if (dialect === 'sqlite') {
expect(results.filter(r => r.name === 'sqlite_autoindex_testSyncs_1')).to.have.length(1);
}
expect(results.filter(r => r.name === 'another_index_email_mobile')).to.have.length(1);
expect(results.filter(r => r.name === 'another_index_phone_mobile')).to.have.length(1);
expect(results.filter(r => r.name === 'another_index_email')).to.have.length(1);
......@@ -225,7 +229,8 @@ describe(Support.getTestDialectTeaser('Model'), () => {
.then(results => {
if (dialect === 'sqlite') {
// SQLite doesn't treat primary key as index
expect(results).to.have.length(4);
// However it does create an extra "autoindex", except primary == false
expect(results).to.have.length(4 + 1);
} else {
expect(results).to.have.length(4 + 1);
expect(results.filter(r => r.primary)).to.have.length(1);
......
......@@ -209,7 +209,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => {
expect(firstForeignKeys.length).to.be.equal(newForeignKeys.length);
expect(firstForeignKeys[0].columnName).to.be.equal('level_id');
expect(firstForeignKeys[0].columnName).to.be.equal(newForeignKeys[0].columnName);
return this.queryInterface.describeTable({
tableName: 'users'
});
......@@ -237,5 +237,136 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => {
});
});
}
if (dialect === 'sqlite') {
it('should not remove unique constraints when adding or modifying columns', function() {
return this.queryInterface.createTable({
tableName: 'Foos'
}, {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER
},
name: {
allowNull: false,
unique: true,
type: DataTypes.STRING
},
email: {
allowNull: false,
unique: true,
type: DataTypes.STRING
}
}).then(() => {
return this.queryInterface.addColumn('Foos', 'phone', {
type: DataTypes.STRING,
defaultValue: null,
allowNull: true
}).then(() => {
return this.queryInterface.describeTable({
tableName: 'Foos'
}).then(table => {
expect(table.phone.allowNull).to.equal(true, '(1) phone column should allow null values');
expect(table.phone.defaultValue).to.equal(null, '(1) phone column should have a default value of null');
expect(table.email.unique).to.equal(true, '(1) email column should remain unique');
expect(table.name.unique).to.equal(true, '(1) name column should remain unique');
}).then(() => {
return this.queryInterface.changeColumn('Foos', 'email', {
type: DataTypes.STRING,
allowNull: true
}).then(() => {
return this.queryInterface.describeTable({
tableName: 'Foos'
}).then(table => {
expect(table.email.allowNull).to.equal(true, '(2) email column should allow null values');
expect(table.email.unique).to.equal(true, '(2) email column should remain unique');
expect(table.name.unique).to.equal(true, '(2) name column should remain unique');
});
});
});
});
});
});
it('should add unique constraints to 2 columns and keep allowNull', function() {
return this.queryInterface.createTable({
tableName: 'Foos'
}, {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER
},
name: {
allowNull: false,
type: DataTypes.STRING
},
email: {
allowNull: true,
type: DataTypes.STRING
}
}).then(() => {
return this.queryInterface.changeColumn('Foos', 'name', {
type: DataTypes.STRING,
unique: true
}).then(() => {
return this.queryInterface.changeColumn('Foos', 'email', {
type: DataTypes.STRING,
unique: true
}).then(() => {
return this.queryInterface.describeTable({
tableName: 'Foos'
}).then(table => {
expect(table.name.allowNull).to.equal(false);
expect(table.name.unique).to.equal(true);
expect(table.email.allowNull).to.equal(true);
expect(table.email.unique).to.equal(true);
});
});
});
});
});
it('should not remove foreign keys when adding or modifying columns', function() {
const Task = this.sequelize.define('Task', { title: DataTypes.STRING }),
User = this.sequelize.define('User', { username: DataTypes.STRING });
User.hasOne(Task);
return User.sync({ force: true }).then(() => {
return Task.sync({ force: true }).then(() => {
return this.queryInterface.addColumn('Tasks', 'bar', DataTypes.INTEGER).then(() => {
return this.queryInterface.getForeignKeyReferencesForTable('Tasks').then(refs => {
expect(refs.length).to.equal(1, 'should keep foreign key after adding column');
expect(refs[0].columnName).to.equal('UserId');
expect(refs[0].referencedTableName).to.equal('Users');
expect(refs[0].referencedColumnName).to.equal('id');
}).then(() => {
return this.queryInterface.changeColumn('Tasks', 'bar', DataTypes.STRING).then(() => {
return this.queryInterface.getForeignKeyReferencesForTable('Tasks').then(refs => {
expect(refs.length).to.equal(1, 'should keep foreign key after changing column');
expect(refs[0].columnName).to.equal('UserId');
expect(refs[0].referencedTableName).to.equal('Users');
expect(refs[0].referencedColumnName).to.equal('id');
}).then(() => {
return this.queryInterface.renameColumn('Tasks', 'bar', 'foo').then(() => {
return this.queryInterface.getForeignKeyReferencesForTable('Tasks').then(refs => {
expect(refs.length).to.equal(1, 'should keep foreign key after renaming column');
expect(refs[0].columnName).to.equal('UserId');
expect(refs[0].referencedTableName).to.equal('Users');
expect(refs[0].referencedColumnName).to.equal('id');
});
});
});
});
});
});
});
});
});
}
});
});
......@@ -606,7 +606,7 @@ if (dialect === 'sqlite') {
title: 'Properly quotes column names',
arguments: ['myTable', 'foo', 'commit', { commit: 'VARCHAR(255)', bar: 'VARCHAR(255)' }],
expectation:
'CREATE TEMPORARY TABLE IF NOT EXISTS `myTable_backup` (`commit` VARCHAR(255), `bar` VARCHAR(255));' +
'CREATE TABLE IF NOT EXISTS `myTable_backup` (`commit` VARCHAR(255), `bar` VARCHAR(255));' +
'INSERT INTO `myTable_backup` SELECT `foo` AS `commit`, `bar` FROM `myTable`;' +
'DROP TABLE `myTable`;' +
'CREATE TABLE IF NOT EXISTS `myTable` (`commit` VARCHAR(255), `bar` VARCHAR(255));' +
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!