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

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 { ...@@ -434,7 +434,8 @@ class SQLiteQueryGenerator extends MySqlQueryGenerator {
).join(', '); ).join(', ');
const attributeNamesExport = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).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};` }INSERT INTO ${quotedBackupTableName} SELECT ${attributeNamesImport} FROM ${quotedTableName};`
+ `DROP TABLE ${quotedTableName};${ + `DROP TABLE ${quotedTableName};${
this.createTableQuery(tableName, attributes) this.createTableQuery(tableName, attributes)
......
...@@ -60,7 +60,7 @@ function changeColumn(qi, tableName, attributes, options) { ...@@ -60,7 +60,7 @@ function changeColumn(qi, tableName, attributes, options) {
options = options || {}; options = options || {};
return qi.describeTable(tableName, options).then(fields => { 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 sql = qi.QueryGenerator.removeColumnQuery(tableName, fields);
const subQueries = sql.split(';').filter(q => q !== ''); const subQueries = sql.split(';').filter(q => q !== '');
...@@ -206,3 +206,94 @@ function getForeignKeyReferencesForTable(qi, tableName, options) { ...@@ -206,3 +206,94 @@ function getForeignKeyReferencesForTable(qi, tableName, options) {
} }
exports.getForeignKeyReferencesForTable = getForeignKeyReferencesForTable; 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 { ...@@ -450,6 +450,11 @@ class QueryInterface {
* @returns {Promise<Object>} * @returns {Promise<Object>}
*/ */
describeTable(tableName, options) { 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 schema = null;
let schemaDelimiter = null; let schemaDelimiter = null;
......
...@@ -2376,7 +2376,11 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -2376,7 +2376,11 @@ describe(Support.getTestDialectTeaser('Model'), () => {
return UserPublic.schema('special').sync({ force: true }).then(() => { return UserPublic.schema('special').sync({ force: true }).then(() => {
return this.sequelize.queryInterface.describeTable('Publics', { return this.sequelize.queryInterface.describeTable('Publics', {
logging(sql) { 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'); expect(sql).to.not.contain('special');
count++; count++;
} }
...@@ -2389,7 +2393,11 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -2389,7 +2393,11 @@ describe(Support.getTestDialectTeaser('Model'), () => {
return this.sequelize.queryInterface.describeTable('Publics', { return this.sequelize.queryInterface.describeTable('Publics', {
schema: 'special', schema: 'special',
logging(sql) { 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'); expect(sql).to.contain('special');
count++; count++;
} }
......
...@@ -184,12 +184,16 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -184,12 +184,16 @@ describe(Support.getTestDialectTeaser('Model'), () => {
.then(results => { .then(results => {
if (dialect === 'sqlite') { if (dialect === 'sqlite') {
// SQLite doesn't treat primary key as index // 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 { } else {
expect(results).to.have.length(4 + 1); expect(results).to.have.length(4 + 1);
expect(results.filter(r => r.primary)).to.have.length(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_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_phone_mobile')).to.have.length(1);
expect(results.filter(r => r.name === 'another_index_email')).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'), () => { ...@@ -225,7 +229,8 @@ describe(Support.getTestDialectTeaser('Model'), () => {
.then(results => { .then(results => {
if (dialect === 'sqlite') { if (dialect === 'sqlite') {
// SQLite doesn't treat primary key as index // 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 { } else {
expect(results).to.have.length(4 + 1); expect(results).to.have.length(4 + 1);
expect(results.filter(r => r.primary)).to.have.length(1); expect(results.filter(r => r.primary)).to.have.length(1);
......
...@@ -237,5 +237,136 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { ...@@ -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') { ...@@ -606,7 +606,7 @@ if (dialect === 'sqlite') {
title: 'Properly quotes column names', title: 'Properly quotes column names',
arguments: ['myTable', 'foo', 'commit', { commit: 'VARCHAR(255)', bar: 'VARCHAR(255)' }], arguments: ['myTable', 'foo', 'commit', { commit: 'VARCHAR(255)', bar: 'VARCHAR(255)' }],
expectation: 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`;' + 'INSERT INTO `myTable_backup` SELECT `foo` AS `commit`, `bar` FROM `myTable`;' +
'DROP TABLE `myTable`;' + 'DROP TABLE `myTable`;' +
'CREATE TABLE IF NOT EXISTS `myTable` (`commit` VARCHAR(255), `bar` VARCHAR(255));' + '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!