remove-column.test.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import { DataTypes } from '@sequelize/core';
  2. import { expect } from 'chai';
  3. import { getTestDialect, getTestDialectTeaser, sequelize } from '../support';
  4. const dialectName = getTestDialect();
  5. const queryInterface = sequelize.queryInterface;
  6. describe(getTestDialectTeaser('QueryInterface#removeColumn'), () => {
  7. describe('Without schema', () => {
  8. beforeEach(async () => {
  9. await queryInterface.createTable('users', {
  10. id: {
  11. type: DataTypes.INTEGER,
  12. primaryKey: true,
  13. autoIncrement: true,
  14. },
  15. firstName: {
  16. type: DataTypes.STRING,
  17. defaultValue: 'Someone',
  18. },
  19. lastName: {
  20. type: DataTypes.STRING,
  21. },
  22. manager: {
  23. type: DataTypes.INTEGER,
  24. references: {
  25. table: 'users',
  26. key: 'id',
  27. },
  28. },
  29. email: {
  30. type: DataTypes.STRING,
  31. allowNull: false,
  32. unique: true,
  33. },
  34. });
  35. });
  36. if (sequelize.dialect.supports.removeColumn.cascade) {
  37. it('should be able to remove a column with cascade', async () => {
  38. await queryInterface.createTable('level', {
  39. id: {
  40. type: DataTypes.INTEGER,
  41. primaryKey: true,
  42. autoIncrement: true,
  43. },
  44. name: {
  45. type: DataTypes.STRING,
  46. allowNull: false,
  47. },
  48. });
  49. await queryInterface.addColumn('users', 'level_id', {
  50. type: DataTypes.INTEGER,
  51. references: { key: 'id', table: 'level' },
  52. });
  53. await queryInterface.removeColumn('level', 'id', { cascade: true });
  54. const levelTable = await queryInterface.describeTable('level');
  55. expect(levelTable).to.not.have.property('id');
  56. const usersTable = await queryInterface.describeTable('users');
  57. expect(usersTable).to.have.property('level_id');
  58. });
  59. }
  60. if (sequelize.dialect.supports.removeColumn.ifExists) {
  61. it('should not throw an error for a non-existant column', async () => {
  62. await queryInterface.removeColumn('users', 'bla', { ifExists: true });
  63. });
  64. }
  65. it('should be able to remove a column with a default value', async () => {
  66. await queryInterface.removeColumn('users', 'firstName');
  67. const table = await queryInterface.describeTable('users');
  68. expect(table).to.not.have.property('firstName');
  69. });
  70. it('should be able to remove a column without default value', async () => {
  71. await queryInterface.removeColumn('users', 'lastName');
  72. const table = await queryInterface.describeTable('users');
  73. expect(table).to.not.have.property('lastName');
  74. });
  75. it('should be able to remove a column with a foreign key constraint', async () => {
  76. await queryInterface.removeColumn('users', 'manager');
  77. const table = await queryInterface.describeTable('users');
  78. expect(table).to.not.have.property('manager');
  79. });
  80. it('should be able to remove a column with primaryKey', async () => {
  81. await queryInterface.removeColumn('users', 'manager');
  82. const table0 = await queryInterface.describeTable('users');
  83. expect(table0).to.not.have.property('manager');
  84. await queryInterface.removeColumn('users', 'id');
  85. const table = await queryInterface.describeTable('users');
  86. expect(table).to.not.have.property('id');
  87. });
  88. // From MSSQL documentation on ALTER COLUMN:
  89. // The modified column cannot be any one of the following:
  90. // - Used in a CHECK or UNIQUE constraint.
  91. // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-table-transact-sql#arguments
  92. if (dialectName !== 'mssql') {
  93. it('should be able to remove a column with unique contraint', async () => {
  94. await queryInterface.removeColumn('users', 'email');
  95. const table = await queryInterface.describeTable('users');
  96. expect(table).to.not.have.property('email');
  97. });
  98. }
  99. // sqlite has limited ALTER TABLE capapibilites which requires a workaround involving recreating tables.
  100. // This leads to issues with losing data or losing foreign key references.
  101. // The tests below address these problems
  102. it('should remove a column with from table with foreign key constraints without losing data', async () => {
  103. await queryInterface.createTable('level', {
  104. id: {
  105. type: DataTypes.INTEGER,
  106. primaryKey: true,
  107. autoIncrement: true,
  108. },
  109. name: {
  110. type: DataTypes.STRING,
  111. allowNull: false,
  112. },
  113. });
  114. await queryInterface.createTable('actors', {
  115. id: {
  116. type: DataTypes.INTEGER,
  117. primaryKey: true,
  118. autoIncrement: true,
  119. },
  120. name: {
  121. type: DataTypes.STRING,
  122. allowNull: true,
  123. },
  124. level_id: {
  125. type: DataTypes.INTEGER,
  126. allowNull: false,
  127. references: {
  128. key: 'id',
  129. table: 'level',
  130. },
  131. onDelete: 'CASCADE',
  132. onUpdate: 'CASCADE',
  133. },
  134. });
  135. await queryInterface.bulkInsert('level', [{ name: 'L1' }, { name: 'L2' }, { name: 'L3' }]);
  136. await queryInterface.bulkInsert('actors', [
  137. { name: 'Keanu Reeves', level_id: 2 },
  138. { name: 'Laurence Fishburne', level_id: 1 },
  139. ]);
  140. await queryInterface.removeColumn('level', 'name');
  141. const actors = await queryInterface.select(null, 'actors', {});
  142. expect(actors).to.deep.equal([
  143. { id: 1, name: 'Keanu Reeves', level_id: 2 },
  144. { id: 2, name: 'Laurence Fishburne', level_id: 1 },
  145. ]);
  146. });
  147. it('should retain ON UPDATE and ON DELETE constraints after a column is removed', async () => {
  148. await queryInterface.createTable('level', {
  149. id: {
  150. type: DataTypes.INTEGER,
  151. primaryKey: true,
  152. autoIncrement: true,
  153. },
  154. name: {
  155. type: DataTypes.STRING,
  156. allowNull: false,
  157. },
  158. });
  159. await queryInterface.createTable('actors', {
  160. id: {
  161. type: DataTypes.INTEGER,
  162. primaryKey: true,
  163. autoIncrement: true,
  164. },
  165. name: {
  166. type: DataTypes.STRING,
  167. allowNull: true,
  168. },
  169. level_id: {
  170. type: DataTypes.INTEGER,
  171. allowNull: false,
  172. },
  173. });
  174. await queryInterface.addConstraint('actors', {
  175. name: 'actors_level_id_fkey',
  176. type: 'FOREIGN KEY',
  177. fields: ['level_id'],
  178. references: { field: 'id', table: 'level' },
  179. onDelete: 'CASCADE',
  180. });
  181. await queryInterface.removeColumn('actors', 'name');
  182. const defaultSchema = sequelize.dialect.getDefaultSchema();
  183. const constraints = await queryInterface.showConstraints('actors', {
  184. constraintType: 'FOREIGN KEY',
  185. });
  186. expect(constraints).to.deep.equal([
  187. {
  188. ...(['mssql', 'postgres'].includes(dialectName) && {
  189. constraintCatalog: 'sequelize_test',
  190. }),
  191. constraintSchema: defaultSchema,
  192. constraintName: dialectName === 'sqlite3' ? 'FOREIGN' : 'actors_level_id_fkey',
  193. constraintType: 'FOREIGN KEY',
  194. ...(['mssql', 'postgres'].includes(dialectName) && { tableCatalog: 'sequelize_test' }),
  195. tableSchema: defaultSchema,
  196. tableName: 'actors',
  197. columnNames: ['level_id'],
  198. referencedTableName: 'level',
  199. referencedTableSchema: defaultSchema,
  200. referencedColumnNames: ['id'],
  201. deleteAction: 'CASCADE',
  202. updateAction:
  203. dialectName === 'mariadb' ? 'RESTRICT' : dialectName === 'sqlite3' ? '' : 'NO ACTION',
  204. ...(sequelize.dialect.supports.constraints.deferrable && {
  205. deferrable: 'INITIALLY_IMMEDIATE',
  206. }),
  207. },
  208. ]);
  209. });
  210. });
  211. if (sequelize.dialect.supports.schemas) {
  212. describe('With schema', () => {
  213. beforeEach(async () => {
  214. await sequelize.createSchema('archive');
  215. await queryInterface.createTable(
  216. {
  217. tableName: 'users',
  218. schema: 'archive',
  219. },
  220. {
  221. id: {
  222. type: DataTypes.INTEGER,
  223. primaryKey: true,
  224. autoIncrement: true,
  225. },
  226. firstName: {
  227. type: DataTypes.STRING,
  228. defaultValue: 'Someone',
  229. },
  230. lastName: {
  231. type: DataTypes.STRING,
  232. },
  233. email: {
  234. type: DataTypes.STRING,
  235. unique: true,
  236. allowNull: false,
  237. },
  238. },
  239. );
  240. });
  241. it('should be able to remove a column', async () => {
  242. await queryInterface.removeColumn(
  243. {
  244. tableName: 'users',
  245. schema: 'archive',
  246. },
  247. 'lastName',
  248. );
  249. const table = await queryInterface.describeTable({
  250. tableName: 'users',
  251. schema: 'archive',
  252. });
  253. expect(table).to.not.have.property('lastName');
  254. });
  255. });
  256. }
  257. });