changeColumn.test.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. 'use strict';
  2. const chai = require('chai');
  3. const expect = chai.expect;
  4. const Support = require('../support');
  5. const { DataTypes } = require('@sequelize/core');
  6. const dialect = Support.getTestDialect();
  7. describe(Support.getTestDialectTeaser('QueryInterface'), () => {
  8. beforeEach(async function () {
  9. this.queryInterface = this.sequelize.queryInterface;
  10. });
  11. describe('changeColumn', () => {
  12. if (Support.sequelize.dialect.supports.schemas) {
  13. it('should support schemas', async function () {
  14. await this.sequelize.createSchema('archive');
  15. await this.queryInterface.createTable(
  16. {
  17. tableName: 'users',
  18. schema: 'archive',
  19. },
  20. {
  21. id: {
  22. type: DataTypes.INTEGER,
  23. primaryKey: true,
  24. autoIncrement: true,
  25. },
  26. currency: DataTypes.INTEGER,
  27. },
  28. );
  29. await this.queryInterface.changeColumn(
  30. {
  31. tableName: 'users',
  32. schema: 'archive',
  33. },
  34. 'currency',
  35. {
  36. type: DataTypes.FLOAT,
  37. },
  38. );
  39. const table = await this.queryInterface.describeTable({
  40. tableName: 'users',
  41. schema: 'archive',
  42. });
  43. if (['postgres', 'postgres-native', 'mssql', 'db2'].includes(dialect)) {
  44. expect(table.currency.type).to.equal('REAL');
  45. } else {
  46. expect(table.currency.type).to.equal('FLOAT');
  47. }
  48. });
  49. }
  50. it('should change columns', async function () {
  51. await this.queryInterface.createTable(
  52. {
  53. tableName: 'users',
  54. },
  55. {
  56. id: {
  57. type: DataTypes.INTEGER,
  58. primaryKey: true,
  59. autoIncrement: true,
  60. },
  61. currency: DataTypes.INTEGER,
  62. },
  63. );
  64. if (dialect === 'db2') {
  65. // DB2 can change only one attr of a column
  66. await this.queryInterface.changeColumn('users', 'currency', {
  67. type: DataTypes.FLOAT,
  68. });
  69. } else {
  70. await this.queryInterface.changeColumn('users', 'currency', {
  71. type: DataTypes.FLOAT,
  72. allowNull: true,
  73. });
  74. }
  75. const table = await this.queryInterface.describeTable({
  76. tableName: 'users',
  77. });
  78. if (['postgres', 'postgres-native', 'mssql', 'sqlite3', 'db2'].includes(dialect)) {
  79. expect(table.currency.type).to.equal('REAL');
  80. } else {
  81. expect(table.currency.type).to.equal('FLOAT');
  82. }
  83. });
  84. // MSSQL doesn't support using a modified column in a check constraint.
  85. // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-table-transact-sql
  86. if (dialect !== 'mssql' && dialect !== 'db2') {
  87. it('should work with enums (case 1)', async function () {
  88. await this.queryInterface.createTable(
  89. {
  90. tableName: 'users',
  91. },
  92. {
  93. firstName: DataTypes.STRING,
  94. },
  95. );
  96. await this.queryInterface.changeColumn('users', 'firstName', {
  97. type: DataTypes.ENUM(['value1', 'value2', 'value3']),
  98. });
  99. });
  100. it('should work with enums (case 2)', async function () {
  101. await this.queryInterface.createTable(
  102. {
  103. tableName: 'users',
  104. },
  105. {
  106. firstName: DataTypes.STRING,
  107. },
  108. );
  109. await this.queryInterface.changeColumn('users', 'firstName', {
  110. type: DataTypes.ENUM(['value1', 'value2', 'value3']),
  111. });
  112. });
  113. if (Support.sequelize.dialect.supports.schemas) {
  114. it('should work with enums with schemas', async function () {
  115. await this.sequelize.createSchema('archive');
  116. await this.queryInterface.createTable(
  117. {
  118. tableName: 'users',
  119. schema: 'archive',
  120. },
  121. {
  122. firstName: DataTypes.STRING,
  123. },
  124. );
  125. await this.queryInterface.changeColumn(
  126. {
  127. tableName: 'users',
  128. schema: 'archive',
  129. },
  130. 'firstName',
  131. {
  132. type: DataTypes.ENUM(['value1', 'value2', 'value3']),
  133. },
  134. );
  135. });
  136. }
  137. }
  138. describe('should support foreign keys', () => {
  139. beforeEach(async function () {
  140. await this.queryInterface.createTable('users', {
  141. id: {
  142. type: DataTypes.INTEGER,
  143. primaryKey: true,
  144. autoIncrement: true,
  145. },
  146. level_id: {
  147. type: DataTypes.INTEGER,
  148. allowNull: false,
  149. },
  150. });
  151. await this.queryInterface.createTable('level', {
  152. id: {
  153. type: DataTypes.INTEGER,
  154. primaryKey: true,
  155. autoIncrement: true,
  156. },
  157. });
  158. });
  159. it('able to change column to foreign key', async function () {
  160. const foreignKeys = await this.queryInterface.showConstraints('users', {
  161. constraintType: 'FOREIGN KEY',
  162. });
  163. expect(foreignKeys).to.be.an('array');
  164. expect(foreignKeys).to.be.empty;
  165. await this.queryInterface.changeColumn('users', 'level_id', {
  166. type: DataTypes.INTEGER,
  167. references: {
  168. table: 'level',
  169. key: 'id',
  170. },
  171. onUpdate: 'cascade',
  172. onDelete: 'cascade',
  173. });
  174. const newForeignKeys = await this.queryInterface.showConstraints('users', {
  175. constraintType: 'FOREIGN KEY',
  176. });
  177. expect(newForeignKeys).to.be.an('array');
  178. expect(newForeignKeys).to.have.lengthOf(1);
  179. expect(newForeignKeys[0].columnNames).to.deep.equal(['level_id']);
  180. });
  181. it('able to change column property without affecting other properties', async function () {
  182. // 1. look for users table information
  183. // 2. change column level_id on users to have a Foreign Key
  184. // 3. look for users table Foreign Keys information
  185. // 4. change column level_id AGAIN to allow null values
  186. // 5. look for new foreign keys information
  187. // 6. look for new table structure information
  188. // 7. compare foreign keys and tables(before and after the changes)
  189. const firstTable = await this.queryInterface.describeTable({
  190. tableName: 'users',
  191. });
  192. await this.queryInterface.changeColumn('users', 'level_id', {
  193. type: DataTypes.INTEGER,
  194. references: {
  195. table: 'level',
  196. key: 'id',
  197. },
  198. onUpdate: 'cascade',
  199. onDelete: 'cascade',
  200. });
  201. const keys = await this.queryInterface.showConstraints('users', {
  202. constraintType: 'FOREIGN KEY',
  203. });
  204. const firstForeignKeys = keys;
  205. await this.queryInterface.changeColumn('users', 'level_id', {
  206. type: DataTypes.INTEGER,
  207. allowNull: true,
  208. });
  209. const newForeignKeys = await this.queryInterface.showConstraints('users', {
  210. constraintType: 'FOREIGN KEY',
  211. });
  212. expect(firstForeignKeys.length).to.equal(newForeignKeys.length);
  213. expect(firstForeignKeys[0].columnNames).to.deep.equal(['level_id']);
  214. expect(firstForeignKeys[0].columnNames).to.deep.equal(newForeignKeys[0].columnNames);
  215. const describedTable = await this.queryInterface.describeTable({
  216. tableName: 'users',
  217. });
  218. expect(describedTable.level_id).to.have.property('allowNull');
  219. expect(describedTable.level_id.allowNull).to.not.equal(firstTable.level_id.allowNull);
  220. expect(describedTable.level_id.allowNull).to.equal(true);
  221. });
  222. if (!['db2', 'ibmi', 'sqlite3'].includes(dialect)) {
  223. it('should change the comment of column', async function () {
  224. const describedTable = await this.queryInterface.describeTable({
  225. tableName: 'users',
  226. });
  227. expect(describedTable.level_id.comment).to.equal(null);
  228. await this.queryInterface.changeColumn('users', 'level_id', {
  229. type: DataTypes.INTEGER,
  230. comment: 'FooBar',
  231. });
  232. const describedTable2 = await this.queryInterface.describeTable({ tableName: 'users' });
  233. expect(describedTable2.level_id.comment).to.equal('FooBar');
  234. });
  235. }
  236. });
  237. // sqlite has limited ALTER TABLE capapibilites which requires a workaround involving recreating tables.
  238. // This leads to issues with losing data or losing foreign key references.
  239. // The tests below address these problems
  240. // TODO: run in all dialects
  241. if (dialect === 'sqlite3') {
  242. it('should not loose indexes & unique constraints when adding or modifying columns', async function () {
  243. await this.queryInterface.createTable('foos', {
  244. id: {
  245. allowNull: false,
  246. autoIncrement: true,
  247. primaryKey: true,
  248. type: DataTypes.INTEGER,
  249. },
  250. name: {
  251. allowNull: false,
  252. unique: true,
  253. type: DataTypes.STRING,
  254. },
  255. email: {
  256. allowNull: false,
  257. unique: true,
  258. type: DataTypes.STRING,
  259. },
  260. birthday: {
  261. allowNull: false,
  262. type: DataTypes.DATEONLY,
  263. },
  264. });
  265. await this.queryInterface.addIndex('foos', ['birthday']);
  266. const initialIndexes = await this.queryInterface.showIndex('foos');
  267. let table = await this.queryInterface.describeTable('foos');
  268. expect(table.email.unique).to.equal(true, '(0) email column should be unique');
  269. expect(table.name.unique).to.equal(true, '(0) name column should be unique');
  270. await this.queryInterface.addColumn('foos', 'phone', {
  271. type: DataTypes.STRING,
  272. defaultValue: null,
  273. allowNull: true,
  274. });
  275. expect(await this.queryInterface.showIndex('foos')).to.deep.equal(
  276. initialIndexes,
  277. 'addColumn should not modify indexes',
  278. );
  279. table = await this.queryInterface.describeTable('foos');
  280. expect(table.phone.allowNull).to.equal(true, '(1) phone column should allow null values');
  281. expect(table.phone.defaultValue).to.equal(
  282. null,
  283. '(1) phone column should have a default value of null',
  284. );
  285. expect(table.email.unique).to.equal(true, '(1) email column should remain unique');
  286. expect(table.name.unique).to.equal(true, '(1) name column should remain unique');
  287. await this.queryInterface.changeColumn('foos', 'email', {
  288. type: DataTypes.STRING,
  289. allowNull: true,
  290. });
  291. expect(await this.queryInterface.showIndex('foos')).to.deep.equal(
  292. initialIndexes,
  293. 'changeColumn should not modify indexes',
  294. );
  295. table = await this.queryInterface.describeTable('foos');
  296. expect(table.email.allowNull).to.equal(true, '(2) email column should allow null values');
  297. expect(table.email.unique).to.equal(true, '(2) email column should remain unique');
  298. expect(table.name.unique).to.equal(true, '(2) name column should remain unique');
  299. });
  300. it('should add unique constraints to 2 columns and keep allowNull', async function () {
  301. await this.queryInterface.createTable(
  302. {
  303. tableName: 'Foos',
  304. },
  305. {
  306. id: {
  307. allowNull: false,
  308. autoIncrement: true,
  309. primaryKey: true,
  310. type: DataTypes.INTEGER,
  311. },
  312. name: {
  313. allowNull: false,
  314. type: DataTypes.STRING,
  315. },
  316. email: {
  317. allowNull: true,
  318. type: DataTypes.STRING,
  319. },
  320. },
  321. );
  322. await this.queryInterface.changeColumn('Foos', 'name', {
  323. type: DataTypes.STRING,
  324. unique: true,
  325. });
  326. await this.queryInterface.changeColumn('Foos', 'email', {
  327. type: DataTypes.STRING,
  328. unique: true,
  329. });
  330. const table = await this.queryInterface.describeTable({
  331. tableName: 'Foos',
  332. });
  333. expect(table.name.allowNull).to.equal(false);
  334. expect(table.name.unique).to.equal(true);
  335. expect(table.email.allowNull).to.equal(true);
  336. expect(table.email.unique).to.equal(true);
  337. });
  338. it('should not remove foreign keys when adding or modifying columns', async function () {
  339. const Task = this.sequelize.define('Task', { title: DataTypes.STRING });
  340. const User = this.sequelize.define('User', { username: DataTypes.STRING });
  341. User.hasOne(Task);
  342. await User.sync({ force: true });
  343. await Task.sync({ force: true });
  344. await this.queryInterface.addColumn('Tasks', 'bar', DataTypes.INTEGER);
  345. let refs = await this.queryInterface.showConstraints(Task, {
  346. constraintType: 'FOREIGN KEY',
  347. });
  348. expect(refs.length).to.equal(1, 'should keep foreign key after adding column');
  349. expect(refs[0].columnNames).to.deep.equal(['userId']);
  350. expect(refs[0].referencedTableName).to.equal('Users');
  351. expect(refs[0].referencedColumnNames).to.deep.equal(['id']);
  352. await this.queryInterface.changeColumn('Tasks', 'bar', DataTypes.STRING);
  353. refs = await this.queryInterface.showConstraints(Task, { constraintType: 'FOREIGN KEY' });
  354. expect(refs.length).to.equal(1, 'should keep foreign key after changing column');
  355. expect(refs[0].columnNames).to.deep.equal(['userId']);
  356. expect(refs[0].referencedTableName).to.equal('Users');
  357. expect(refs[0].referencedColumnNames).to.deep.equal(['id']);
  358. await this.queryInterface.renameColumn('Tasks', 'bar', 'foo');
  359. refs = await this.queryInterface.showConstraints(Task, { constraintType: 'FOREIGN KEY' });
  360. expect(refs.length).to.equal(1, 'should keep foreign key after renaming column');
  361. expect(refs[0].columnNames).to.deep.equal(['userId']);
  362. expect(refs[0].referencedTableName).to.equal('Users');
  363. expect(refs[0].referencedColumnNames).to.deep.equal(['id']);
  364. });
  365. it('should retain ON UPDATE and ON DELETE constraints after a column is changed', async function () {
  366. await this.queryInterface.createTable('level', {
  367. id: {
  368. type: DataTypes.INTEGER,
  369. primaryKey: true,
  370. autoIncrement: true,
  371. },
  372. name: {
  373. type: DataTypes.STRING,
  374. allowNull: false,
  375. },
  376. });
  377. await this.queryInterface.createTable('users', {
  378. id: {
  379. type: DataTypes.INTEGER,
  380. primaryKey: true,
  381. autoIncrement: true,
  382. },
  383. name: {
  384. type: DataTypes.STRING,
  385. allowNull: true,
  386. },
  387. level_id: {
  388. type: DataTypes.INTEGER,
  389. allowNull: false,
  390. references: {
  391. key: 'id',
  392. table: 'level',
  393. },
  394. onDelete: 'CASCADE',
  395. onUpdate: 'CASCADE',
  396. },
  397. });
  398. await this.queryInterface.changeColumn('users', 'name', {
  399. type: DataTypes.STRING,
  400. allowNull: false,
  401. });
  402. await this.queryInterface.changeColumn('users', 'level_id', {
  403. type: DataTypes.INTEGER,
  404. allowNull: true,
  405. references: {
  406. key: 'id',
  407. table: 'level',
  408. },
  409. onDelete: 'CASCADE',
  410. onUpdate: 'CASCADE',
  411. });
  412. const constraints = await this.queryInterface.showConstraints('users');
  413. const foreignKey = constraints.find(
  414. constraint => constraint.constraintType === 'FOREIGN KEY',
  415. );
  416. expect(foreignKey).to.not.be.undefined;
  417. expect(foreignKey).to.have.property('deleteAction', 'CASCADE');
  418. expect(foreignKey).to.have.property('updateAction', 'CASCADE');
  419. });
  420. it('should change columns with foreign key constraints without data loss', async function () {
  421. await this.queryInterface.createTable('users', {
  422. id: {
  423. type: DataTypes.INTEGER,
  424. primaryKey: true,
  425. autoIncrement: true,
  426. },
  427. name: {
  428. type: DataTypes.INTEGER,
  429. allowNull: true,
  430. },
  431. level_id: {
  432. type: DataTypes.INTEGER,
  433. allowNull: false,
  434. references: {
  435. key: 'id',
  436. table: 'level',
  437. },
  438. onDelete: 'CASCADE',
  439. onUpdate: 'CASCADE',
  440. },
  441. });
  442. await this.queryInterface.createTable('level', {
  443. id: {
  444. type: DataTypes.INTEGER,
  445. primaryKey: true,
  446. autoIncrement: true,
  447. },
  448. name: {
  449. type: DataTypes.STRING,
  450. allowNull: false,
  451. },
  452. });
  453. const levels = [
  454. {
  455. id: 1,
  456. name: 'L1',
  457. },
  458. {
  459. id: 2,
  460. name: 'L2',
  461. },
  462. {
  463. id: 3,
  464. name: 'L3',
  465. },
  466. ];
  467. const users = [
  468. {
  469. name: 'Morpheus',
  470. level_id: 2,
  471. },
  472. {
  473. name: 'Neo',
  474. level_id: 1,
  475. },
  476. ];
  477. await Promise.all([
  478. this.queryInterface.bulkInsert('level', levels),
  479. this.queryInterface.bulkInsert('users', users),
  480. ]);
  481. await this.queryInterface.changeColumn('level', 'name', {
  482. type: DataTypes.STRING,
  483. allowNull: true,
  484. });
  485. const userRows = await this.queryInterface.sequelize.query('SELECT * from users;', {
  486. type: 'SELECT',
  487. });
  488. expect(userRows).to.have.length(users.length, 'user records should be unaffected');
  489. });
  490. }
  491. });
  492. });