diff --git a/server/migrations/v2.17.3-fk-constraints.js b/server/migrations/v2.17.3-fk-constraints.js index a62307a3e3..5f8a5c9a63 100644 --- a/server/migrations/v2.17.3-fk-constraints.js +++ b/server/migrations/v2.17.3-fk-constraints.js @@ -31,19 +31,28 @@ async function up({ context: { queryInterface, logger } }) { { field: 'libraryId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }, { field: 'libraryFolderId', onDelete: 'SET NULL', onUpdate: 'CASCADE' } ] - await changeConstraints(queryInterface, 'libraryItems', libraryItemsConstraints) - logger.info('[2.17.3 migration] Finished updating libraryItems constraints') + if (await changeConstraints(queryInterface, 'libraryItems', libraryItemsConstraints)) { + logger.info('[2.17.3 migration] Finished updating libraryItems constraints') + } else { + logger.info('[2.17.3 migration] No changes needed for libraryItems constraints') + } logger.info('[2.17.3 migration] Updating feeds constraints') const feedsConstraints = [{ field: 'userId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }] - await changeConstraints(queryInterface, 'feeds', feedsConstraints) - logger.info('[2.17.3 migration] Finished updating feeds constraints') + if (await changeConstraints(queryInterface, 'feeds', feedsConstraints)) { + logger.info('[2.17.3 migration] Finished updating feeds constraints') + } else { + logger.info('[2.17.3 migration] No changes needed for feeds constraints') + } if (await queryInterface.tableExists('mediaItemShares')) { logger.info('[2.17.3 migration] Updating mediaItemShares constraints') const mediaItemSharesConstraints = [{ field: 'userId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }] - await changeConstraints(queryInterface, 'mediaItemShares', mediaItemSharesConstraints) - logger.info('[2.17.3 migration] Finished updating mediaItemShares constraints') + if (await changeConstraints(queryInterface, 'mediaItemShares', mediaItemSharesConstraints)) { + logger.info('[2.17.3 migration] Finished updating mediaItemShares constraints') + } else { + logger.info('[2.17.3 migration] No changes needed for mediaItemShares constraints') + } } else { logger.info('[2.17.3 migration] mediaItemShares table does not exist, skipping column change') } @@ -54,18 +63,27 @@ async function up({ context: { queryInterface, logger } }) { { field: 'libraryId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }, { field: 'userId', onDelete: 'SET NULL', onUpdate: 'CASCADE' } ] - await changeConstraints(queryInterface, 'playbackSessions', playbackSessionsConstraints) - logger.info('[2.17.3 migration] Finished updating playbackSessions constraints') + if (await changeConstraints(queryInterface, 'playbackSessions', playbackSessionsConstraints)) { + logger.info('[2.17.3 migration] Finished updating playbackSessions constraints') + } else { + logger.info('[2.17.3 migration] No changes needed for playbackSessions constraints') + } logger.info('[2.17.3 migration] Updating playlistMediaItems constraints') const playlistMediaItemsConstraints = [{ field: 'playlistId', onDelete: 'CASCADE', onUpdate: 'CASCADE' }] - await changeConstraints(queryInterface, 'playlistMediaItems', playlistMediaItemsConstraints) - logger.info('[2.17.3 migration] Finished updating playlistMediaItems constraints') + if (await changeConstraints(queryInterface, 'playlistMediaItems', playlistMediaItemsConstraints)) { + logger.info('[2.17.3 migration] Finished updating playlistMediaItems constraints') + } else { + logger.info('[2.17.3 migration] No changes needed for playlistMediaItems constraints') + } logger.info('[2.17.3 migration] Updating mediaProgresses constraints') const mediaProgressesConstraints = [{ field: 'userId', onDelete: 'CASCADE', onUpdate: 'CASCADE' }] - await changeConstraints(queryInterface, 'mediaProgresses', mediaProgressesConstraints) - logger.info('[2.17.3 migration] Finished updating mediaProgresses constraints') + if (await changeConstraints(queryInterface, 'mediaProgresses', mediaProgressesConstraints)) { + logger.info('[2.17.3 migration] Finished updating mediaProgresses constraints') + } else { + logger.info('[2.17.3 migration] No changes needed for mediaProgresses constraints') + } await execQuery(`COMMIT;`) } catch (error) { @@ -103,59 +121,75 @@ async function down({ context: { queryInterface, logger } }) { * @property {string} onUpdate - The onUpdate constraint */ -const formatFKsPragmaToSequelizeFK = (fk) => { - let onDelete = fk['on_delete'] - let onUpdate = fk['on_update'] - - if (fk.from === 'userId' || fk.from === 'libraryId' || fk.from === 'deviceId') { - onDelete = 'SET NULL' - onUpdate = 'CASCADE' - } +/** + * @typedef SequelizeFKObj + * @property {{ model: string, key: string }} references + * @property {string} onDelete + * @property {string} onUpdate + */ +/** + * @param {Object} fk - The foreign key object from PRAGMA foreign_key_list + * @returns {SequelizeFKObj} - The foreign key object formatted for Sequelize + */ +const formatFKsPragmaToSequelizeFK = (fk) => { return { references: { model: fk.table, key: fk.to }, - constraints: { - onDelete, - onUpdate - } + onDelete: fk['on_delete'], + onUpdate: fk['on_update'] } } /** - * Extends the Sequelize describeTable function to include the foreign keys constraints in sqlite dbs + * * @param {import('sequelize').QueryInterface} queryInterface - * @param {String} tableName - The table name - * @param {ConstraintUpdateObj[]} constraints - constraints to update + * @param {string} tableName + * @param {ConstraintUpdateObj[]} constraints + * @returns {Promise|null>} */ -async function describeTableWithFKs(queryInterface, tableName, constraints) { +async function getUpdatedForeignKeys(queryInterface, tableName, constraints) { const execQuery = queryInterface.sequelize.query.bind(queryInterface.sequelize) const quotedTableName = queryInterface.quoteIdentifier(tableName) const foreignKeys = await execQuery(`PRAGMA foreign_key_list(${quotedTableName});`) + let hasUpdates = false const foreignKeysByColName = foreignKeys.reduce((prev, curr) => { const fk = formatFKsPragmaToSequelizeFK(curr) + + const constraint = constraints.find((c) => c.field === curr.from) + if (constraint && (constraint.onDelete !== fk.onDelete || constraint.onUpdate !== fk.onUpdate)) { + fk.onDelete = constraint.onDelete + fk.onUpdate = constraint.onUpdate + hasUpdates = true + } + return { ...prev, [curr.from]: fk } }, {}) + return hasUpdates ? foreignKeysByColName : null +} + +/** + * Extends the Sequelize describeTable function to include the updated foreign key constraints + * + * @param {import('sequelize').QueryInterface} queryInterface + * @param {String} tableName + * @param {Record} updatedForeignKeys + */ +async function describeTableWithFKs(queryInterface, tableName, updatedForeignKeys) { const tableDescription = await queryInterface.describeTable(tableName) const tableDescriptionWithFks = Object.entries(tableDescription).reduce((prev, [col, attributes]) => { let extendedAttributes = attributes - if (foreignKeysByColName[col]) { - // Use the constraints from the constraints array if they exist, otherwise use the existing constraints - const onDelete = constraints.find((c) => c.field === col)?.onDelete || foreignKeysByColName[col].constraints.onDelete - const onUpdate = constraints.find((c) => c.field === col)?.onUpdate || foreignKeysByColName[col].constraints.onUpdate - + if (updatedForeignKeys[col]) { extendedAttributes = { ...extendedAttributes, - references: foreignKeysByColName[col].references, - onDelete, - onUpdate + ...updatedForeignKeys[col] } } return { ...prev, [col]: extendedAttributes } @@ -171,8 +205,14 @@ async function describeTableWithFKs(queryInterface, tableName, constraints) { * @param {import('sequelize').QueryInterface} queryInterface * @param {string} tableName * @param {ConstraintUpdateObj[]} constraints + * @returns {Promise} - Return false if no changes are needed, true otherwise */ async function changeConstraints(queryInterface, tableName, constraints) { + const updatedForeignKeys = await getUpdatedForeignKeys(queryInterface, tableName, constraints) + if (!updatedForeignKeys) { + return false + } + const execQuery = queryInterface.sequelize.query.bind(queryInterface.sequelize) const quotedTableName = queryInterface.quoteIdentifier(tableName) @@ -180,7 +220,7 @@ async function changeConstraints(queryInterface, tableName, constraints) { const quotedBackupTableName = queryInterface.quoteIdentifier(backupTableName) try { - const tableDescriptionWithFks = await describeTableWithFKs(queryInterface, tableName, constraints) + const tableDescriptionWithFks = await describeTableWithFKs(queryInterface, tableName, updatedForeignKeys) const attributes = queryInterface.queryGenerator.attributesToSQL(tableDescriptionWithFks) @@ -210,7 +250,7 @@ async function changeConstraints(queryInterface, tableName, constraints) { return Promise.reject(`Foreign key violations detected: ${JSON.stringify(result, null, 2)}`) } - return Promise.resolve() + return true } catch (error) { return Promise.reject(error) } diff --git a/test/server/migrations/v2.17.3-fk-constraints.test.js b/test/server/migrations/v2.17.3-fk-constraints.test.js new file mode 100644 index 0000000000..33be43ce83 --- /dev/null +++ b/test/server/migrations/v2.17.3-fk-constraints.test.js @@ -0,0 +1,230 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const { up } = require('../../../server/migrations/v2.17.3-fk-constraints') +const { Sequelize, QueryInterface } = require('sequelize') +const Logger = require('../../../server/Logger') + +describe('migration-v2.17.3-fk-constraints', () => { + let sequelize + /** @type {QueryInterface} */ + let queryInterface + let loggerInfoStub + + beforeEach(() => { + sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + queryInterface = sequelize.getQueryInterface() + loggerInfoStub = sinon.stub(Logger, 'info') + }) + + afterEach(() => { + sinon.restore() + }) + + describe('up', () => { + beforeEach(async () => { + // Create associated tables: Users, libraries, libraryFolders, playlists, devices + await queryInterface.sequelize.query('CREATE TABLE `users` (`id` UUID PRIMARY KEY);') + await queryInterface.sequelize.query('CREATE TABLE `libraries` (`id` UUID PRIMARY KEY);') + await queryInterface.sequelize.query('CREATE TABLE `libraryFolders` (`id` UUID PRIMARY KEY);') + await queryInterface.sequelize.query('CREATE TABLE `playlists` (`id` UUID PRIMARY KEY);') + await queryInterface.sequelize.query('CREATE TABLE `devices` (`id` UUID PRIMARY KEY);') + }) + + afterEach(async () => { + await queryInterface.dropAllTables() + }) + + it('should fix table foreign key constraints', async () => { + // Create tables with missing foreign key constraints: libraryItems, feeds, mediaItemShares, playbackSessions, playlistMediaItems, mediaProgresses + await queryInterface.sequelize.query('CREATE TABLE `libraryItems` (`id` UUID UNIQUE PRIMARY KEY, `libraryId` UUID REFERENCES `libraries` (`id`), `libraryFolderId` UUID REFERENCES `libraryFolders` (`id`));') + await queryInterface.sequelize.query('CREATE TABLE `feeds` (`id` UUID UNIQUE PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`));') + await queryInterface.sequelize.query('CREATE TABLE `mediaItemShares` (`id` UUID UNIQUE PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`));') + await queryInterface.sequelize.query('CREATE TABLE `playbackSessions` (`id` UUID UNIQUE PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`), `deviceId` UUID REFERENCES `devices` (`id`), `libraryId` UUID REFERENCES `libraries` (`id`));') + await queryInterface.sequelize.query('CREATE TABLE `playlistMediaItems` (`id` UUID UNIQUE PRIMARY KEY, `playlistId` UUID REFERENCES `playlists` (`id`));') + await queryInterface.sequelize.query('CREATE TABLE `mediaProgresses` (`id` UUID UNIQUE PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`));') + + // + // Validate that foreign key constraints are missing + // + let libraryItemsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(libraryItems);`) + expect(libraryItemsForeignKeys).to.have.deep.members([ + { id: 0, seq: 0, table: 'libraryFolders', from: 'libraryFolderId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }, + { id: 1, seq: 0, table: 'libraries', from: 'libraryId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' } + ]) + + let feedsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(feeds);`) + expect(feedsForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }]) + + let mediaItemSharesForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(mediaItemShares);`) + expect(mediaItemSharesForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }]) + + let playbackSessionForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(playbackSessions);`) + expect(playbackSessionForeignKeys).to.deep.equal([ + { id: 0, seq: 0, table: 'libraries', from: 'libraryId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }, + { id: 1, seq: 0, table: 'devices', from: 'deviceId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }, + { id: 2, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' } + ]) + + let playlistMediaItemsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(playlistMediaItems);`) + expect(playlistMediaItemsForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'playlists', from: 'playlistId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }]) + + let mediaProgressesForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(mediaProgresses);`) + expect(mediaProgressesForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }]) + + // + // Insert test data into tables + // + await queryInterface.bulkInsert('users', [{ id: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }]) + await queryInterface.bulkInsert('libraries', [{ id: 'a41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('libraryFolders', [{ id: 'b41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('playlists', [{ id: 'f41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('devices', [{ id: 'g41a40e3-f516-40f5-810d-757ab668ebba' }]) + + await queryInterface.bulkInsert('libraryItems', [{ id: 'c1a96857-48a8-43b6-8966-abc909c55b0f', libraryId: 'a41a40e3-f516-40f5-810d-757ab668ebba', libraryFolderId: 'b41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('feeds', [{ id: 'd1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }]) + await queryInterface.bulkInsert('mediaItemShares', [{ id: 'h1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }]) + await queryInterface.bulkInsert('playbackSessions', [{ id: 'f1a96857-48a8-43b6-8966-abc909c55b0x', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f', deviceId: 'g41a40e3-f516-40f5-810d-757ab668ebba', libraryId: 'a41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('playlistMediaItems', [{ id: 'i1a96857-48a8-43b6-8966-abc909c55b0f', playlistId: 'f41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('mediaProgresses', [{ id: 'j1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }]) + + // + // Query data before migration + // + const libraryItems = await queryInterface.sequelize.query('SELECT * FROM libraryItems;') + const feeds = await queryInterface.sequelize.query('SELECT * FROM feeds;') + const mediaItemShares = await queryInterface.sequelize.query('SELECT * FROM mediaItemShares;') + const playbackSessions = await queryInterface.sequelize.query('SELECT * FROM playbackSessions;') + const playlistMediaItems = await queryInterface.sequelize.query('SELECT * FROM playlistMediaItems;') + const mediaProgresses = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses;') + + // + // Run migration + // + await up({ context: { queryInterface, logger: Logger } }) + + // + // Validate that foreign key constraints are updated + // + libraryItemsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(libraryItems);`) + expect(libraryItemsForeignKeys).to.have.deep.members([ + { id: 0, seq: 0, table: 'libraryFolders', from: 'libraryFolderId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' }, + { id: 1, seq: 0, table: 'libraries', from: 'libraryId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' } + ]) + + feedsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(feeds);`) + expect(feedsForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' }]) + + mediaItemSharesForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(mediaItemShares);`) + expect(mediaItemSharesForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' }]) + + playbackSessionForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(playbackSessions);`) + expect(playbackSessionForeignKeys).to.deep.equal([ + { id: 0, seq: 0, table: 'libraries', from: 'libraryId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' }, + { id: 1, seq: 0, table: 'devices', from: 'deviceId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' }, + { id: 2, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' } + ]) + + playlistMediaItemsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(playlistMediaItems);`) + expect(playlistMediaItemsForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'playlists', from: 'playlistId', to: 'id', on_update: 'CASCADE', on_delete: 'CASCADE', match: 'NONE' }]) + + mediaProgressesForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(mediaProgresses);`) + expect(mediaProgressesForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'CASCADE', on_delete: 'CASCADE', match: 'NONE' }]) + + // + // Validate that data is not changed + // + const libraryItemsAfter = await queryInterface.sequelize.query('SELECT * FROM libraryItems;') + expect(libraryItemsAfter).to.deep.equal(libraryItems) + + const feedsAfter = await queryInterface.sequelize.query('SELECT * FROM feeds;') + expect(feedsAfter).to.deep.equal(feeds) + + const mediaItemSharesAfter = await queryInterface.sequelize.query('SELECT * FROM mediaItemShares;') + expect(mediaItemSharesAfter).to.deep.equal(mediaItemShares) + + const playbackSessionsAfter = await queryInterface.sequelize.query('SELECT * FROM playbackSessions;') + expect(playbackSessionsAfter).to.deep.equal(playbackSessions) + + const playlistMediaItemsAfter = await queryInterface.sequelize.query('SELECT * FROM playlistMediaItems;') + expect(playlistMediaItemsAfter).to.deep.equal(playlistMediaItems) + + const mediaProgressesAfter = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses;') + expect(mediaProgressesAfter).to.deep.equal(mediaProgresses) + }) + + it('should keep correct table foreign key constraints', async () => { + // Create tables with correct foreign key constraints: libraryItems, feeds, mediaItemShares, playbackSessions, playlistMediaItems, mediaProgresses + await queryInterface.sequelize.query('CREATE TABLE `libraryItems` (`id` UUID PRIMARY KEY, `libraryId` UUID REFERENCES `libraries` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, `libraryFolderId` UUID REFERENCES `libraryFolders` (`id`) ON DELETE SET NULL ON UPDATE CASCADE);') + await queryInterface.sequelize.query('CREATE TABLE `feeds` (`id` UUID PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE);') + await queryInterface.sequelize.query('CREATE TABLE `mediaItemShares` (`id` UUID PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE);') + await queryInterface.sequelize.query('CREATE TABLE `playbackSessions` (`id` UUID PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, `deviceId` UUID REFERENCES `devices` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, `libraryId` UUID REFERENCES `libraries` (`id`) ON DELETE SET NULL ON UPDATE CASCADE);') + await queryInterface.sequelize.query('CREATE TABLE `playlistMediaItems` (`id` UUID PRIMARY KEY, `playlistId` UUID REFERENCES `playlists` (`id`) ON DELETE CASCADE ON UPDATE CASCADE);') + await queryInterface.sequelize.query('CREATE TABLE `mediaProgresses` (`id` UUID PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE);') + + // + // Insert test data into tables + // + await queryInterface.bulkInsert('users', [{ id: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }]) + await queryInterface.bulkInsert('libraries', [{ id: 'a41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('libraryFolders', [{ id: 'b41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('playlists', [{ id: 'f41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('devices', [{ id: 'g41a40e3-f516-40f5-810d-757ab668ebba' }]) + + await queryInterface.bulkInsert('libraryItems', [{ id: 'c1a96857-48a8-43b6-8966-abc909c55b0f', libraryId: 'a41a40e3-f516-40f5-810d-757ab668ebba', libraryFolderId: 'b41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('feeds', [{ id: 'd1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }]) + await queryInterface.bulkInsert('mediaItemShares', [{ id: 'h1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }]) + await queryInterface.bulkInsert('playbackSessions', [{ id: 'f1a96857-48a8-43b6-8966-abc909c55b0x', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f', deviceId: 'g41a40e3-f516-40f5-810d-757ab668ebba', libraryId: 'a41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('playlistMediaItems', [{ id: 'i1a96857-48a8-43b6-8966-abc909c55b0f', playlistId: 'f41a40e3-f516-40f5-810d-757ab668ebba' }]) + await queryInterface.bulkInsert('mediaProgresses', [{ id: 'j1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }]) + + // + // Query data before migration + // + const libraryItems = await queryInterface.sequelize.query('SELECT * FROM libraryItems;') + const feeds = await queryInterface.sequelize.query('SELECT * FROM feeds;') + const mediaItemShares = await queryInterface.sequelize.query('SELECT * FROM mediaItemShares;') + const playbackSessions = await queryInterface.sequelize.query('SELECT * FROM playbackSessions;') + const playlistMediaItems = await queryInterface.sequelize.query('SELECT * FROM playlistMediaItems;') + const mediaProgresses = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses;') + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(14) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.17.3 migration] UPGRADE BEGIN: 2.17.3-fk-constraints'))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.17.3 migration] Updating libraryItems constraints'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.17.3 migration] No changes needed for libraryItems constraints'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.17.3 migration] Updating feeds constraints'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.17.3 migration] No changes needed for feeds constraints'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.17.3 migration] Updating mediaItemShares constraints'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.17.3 migration] No changes needed for mediaItemShares constraints'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.17.3 migration] Updating playbackSessions constraints'))).to.be.true + expect(loggerInfoStub.getCall(8).calledWith(sinon.match('[2.17.3 migration] No changes needed for playbackSessions constraints'))).to.be.true + expect(loggerInfoStub.getCall(9).calledWith(sinon.match('[2.17.3 migration] Updating playlistMediaItems constraints'))).to.be.true + expect(loggerInfoStub.getCall(10).calledWith(sinon.match('[2.17.3 migration] No changes needed for playlistMediaItems constraints'))).to.be.true + expect(loggerInfoStub.getCall(11).calledWith(sinon.match('[2.17.3 migration] Updating mediaProgresses constraints'))).to.be.true + expect(loggerInfoStub.getCall(12).calledWith(sinon.match('[2.17.3 migration] No changes needed for mediaProgresses constraints'))).to.be.true + expect(loggerInfoStub.getCall(13).calledWith(sinon.match('[2.17.3 migration] UPGRADE END: 2.17.3-fk-constraints'))).to.be.true + + // + // Validate that data is not changed + // + const libraryItemsAfter = await queryInterface.sequelize.query('SELECT * FROM libraryItems;') + expect(libraryItemsAfter).to.deep.equal(libraryItems) + + const feedsAfter = await queryInterface.sequelize.query('SELECT * FROM feeds;') + expect(feedsAfter).to.deep.equal(feeds) + + const mediaItemSharesAfter = await queryInterface.sequelize.query('SELECT * FROM mediaItemShares;') + expect(mediaItemSharesAfter).to.deep.equal(mediaItemShares) + + const playbackSessionsAfter = await queryInterface.sequelize.query('SELECT * FROM playbackSessions;') + expect(playbackSessionsAfter).to.deep.equal(playbackSessions) + + const playlistMediaItemsAfter = await queryInterface.sequelize.query('SELECT * FROM playlistMediaItems;') + expect(playlistMediaItemsAfter).to.deep.equal(playlistMediaItems) + + const mediaProgressesAfter = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses;') + expect(mediaProgressesAfter).to.deep.equal(mediaProgresses) + }) + }) +})