diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md index acccef90d..198226637 100644 --- a/server/migrations/changelog.md +++ b/server/migrations/changelog.md @@ -14,3 +14,4 @@ Please add a record of every database migration that you create to this file. Th | v2.17.6 | v2.17.6-share-add-isdownloadable | Adds the isDownloadable column to the mediaItemShares table | | v2.17.7 | v2.17.7-add-indices | Adds indices to the libraryItems and books tables to reduce query times | | v2.19.1 | v2.19.1-copy-title-to-library-items | Copies title and titleIgnorePrefix to the libraryItems table, creates update triggers and indices | +| v2.19.2 | v2.19.2-change-ambigous-column-names | Changes the ambiguous libraryItems column names introduced in v2.19.1 to unambiguous ones | diff --git a/server/migrations/v2.19.2-change-ambigous-column-names.js b/server/migrations/v2.19.2-change-ambigous-column-names.js new file mode 100644 index 000000000..b85aa63c7 --- /dev/null +++ b/server/migrations/v2.19.2-change-ambigous-column-names.js @@ -0,0 +1,180 @@ +const util = require('util') + +/** + * @typedef MigrationContext + * @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object. + * @property {import('../Logger')} logger - a Logger object. + * + * @typedef MigrationOptions + * @property {MigrationContext} context - an object containing the migration context. + */ + +const migrationVersion = '2.19.2' +const migrationName = `${migrationVersion}-change-ambigous-column-names` +const loggerPrefix = `[${migrationVersion} migration]` + +/** + * This upward migration changes the ambiguous column names in libraryItems added in v2.19.1 + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function up({ context: { queryInterface, logger } }) { + // Upwards migration script + logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`) + + // Remove ambiguous columns added in v2.19.1 (including triggers and indexes) + await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'title']) + await removeTrigger(queryInterface, logger, 'libraryItems', 'title') + await removeColumn(queryInterface, logger, 'libraryItems', 'title') + + await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'titleIgnorePrefix']) + await removeTrigger(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix') + await removeColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix') + + // Add new columns with unambiguous names (including triggers and indexes) + await addColumn(queryInterface, logger, 'libraryItems', 'titleCopy', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true }) + await copyColumn(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'titleCopy', 'mediaId') + await addTrigger(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'titleCopy', 'mediaId') + await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'titleCopy', collate: 'NOCASE' }]) + + await addColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefixCopy', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true }) + await copyColumn(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefixCopy', 'mediaId') + await addTrigger(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefixCopy', 'mediaId') + await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'titleIgnorePrefixCopy', collate: 'NOCASE' }]) + + logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`) +} + +/** + * This downward migration script removes the titleCopy and titleIgnorePrefixCopy columns from the libraryItems table, + * and restores the title and titleIgnorePrefix columns, including associated triggers and indexes. + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function down({ context: { queryInterface, logger } }) { + // Downward migration script + logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`) + + // Remove v2.19.2 new columns (including triggers and indexes) + await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'titleIgnorePrefixCopy']) + await removeTrigger(queryInterface, logger, 'libraryItems', 'titleIgnorePrefixCopy') + await removeColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefixCopy') + + await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'titleCopy']) + await removeTrigger(queryInterface, logger, 'libraryItems', 'titleCopy') + await removeColumn(queryInterface, logger, 'libraryItems', 'titleCopy') + + // Restore v2.19.1 columns (including triggers and indexes) + await addColumn(queryInterface, logger, 'libraryItems', 'title', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true }) + await copyColumn(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'title', 'mediaId') + await addTrigger(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'title', 'mediaId') + await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'title', collate: 'NOCASE' }]) + + await addColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true }) + await copyColumn(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId') + await addTrigger(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId') + await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }]) + + logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`) +} + +/** + * Utility function to add an index to a table. If the index already z`exists, it logs a message and continues. + * + * @param {import('sequelize').QueryInterface} queryInterface + * @param {import ('../Logger')} logger + * @param {string} tableName + * @param {string[]} columns + */ +async function addIndex(queryInterface, logger, tableName, columns) { + const columnString = columns.map((column) => util.inspect(column)).join(', ') + const indexName = convertToSnakeCase(`${tableName}_${columns.map((column) => (typeof column === 'string' ? column : column.name)).join('_')}`) + try { + logger.info(`${loggerPrefix} adding index on [${columnString}] to table ${tableName}. index name: ${indexName}"`) + await queryInterface.addIndex(tableName, columns) + logger.info(`${loggerPrefix} added index on [${columnString}] to table ${tableName}. index name: ${indexName}"`) + } catch (error) { + if (error.name === 'SequelizeDatabaseError' && error.message.includes('already exists')) { + logger.info(`${loggerPrefix} index [${columnString}] for table "${tableName}" already exists`) + } else { + throw error + } + } +} + +/** + * Utility function to remove an index from a table. + * Sequelize implemets it using DROP INDEX IF EXISTS, so it won't throw an error if the index doesn't exist. + * + * @param {import('sequelize').QueryInterface} queryInterface + * @param {import ('../Logger')} logger + * @param {string} tableName + * @param {string[]} columns + */ +async function removeIndex(queryInterface, logger, tableName, columns) { + logger.info(`${loggerPrefix} removing index [${columns.join(', ')}] from table "${tableName}"`) + await queryInterface.removeIndex(tableName, columns) + logger.info(`${loggerPrefix} removed index [${columns.join(', ')}] from table "${tableName}"`) +} + +async function addColumn(queryInterface, logger, table, column, options) { + logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`) + const tableDescription = await queryInterface.describeTable(table) + if (!tableDescription[column]) { + await queryInterface.addColumn(table, column, options) + logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`) + } else { + logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`) + } +} + +async function removeColumn(queryInterface, logger, table, column) { + logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`) + await queryInterface.removeColumn(table, column) + logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`) +} + +async function copyColumn(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) { + logger.info(`${loggerPrefix} copying column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`) + await queryInterface.sequelize.query(` + UPDATE ${targetTable} + SET ${targetColumn} = ${sourceTable}.${sourceColumn} + FROM ${sourceTable} + WHERE ${targetTable}.${targetIdColumn} = ${sourceTable}.${sourceIdColumn} + `) + logger.info(`${loggerPrefix} copied column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`) +} + +async function addTrigger(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) { + logger.info(`${loggerPrefix} adding trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`) + const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`) + + await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`) + + await queryInterface.sequelize.query(` + CREATE TRIGGER ${triggerName} + AFTER UPDATE OF ${sourceColumn} ON ${sourceTable} + FOR EACH ROW + BEGIN + UPDATE ${targetTable} + SET ${targetColumn} = NEW.${sourceColumn} + WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn}; + END; + `) + logger.info(`${loggerPrefix} added trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`) +} + +async function removeTrigger(queryInterface, logger, targetTable, targetColumn) { + logger.info(`${loggerPrefix} removing trigger to update ${targetTable}.${targetColumn}`) + const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`) + await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`) + logger.info(`${loggerPrefix} removed trigger to update ${targetTable}.${targetColumn}`) +} + +function convertToSnakeCase(str) { + return str.replace(/([A-Z])/g, '_$1').toLowerCase() +} + +module.exports = { up, down } diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index ace6af434..d1d14c22f 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -74,9 +74,9 @@ class LibraryItem extends Model { /** @type {Book.BookExpanded|Podcast.PodcastExpanded} - only set when expanded */ this.media /** @type {string} */ - this.title // Only used for sorting + this.titleCopy // Only used for sorting /** @type {string} */ - this.titleIgnorePrefix // Only used for sorting + this.titleIgnorePrefixCopy // Only used for sorting } /** @@ -682,8 +682,8 @@ class LibraryItem extends Model { lastScanVersion: DataTypes.STRING, libraryFiles: DataTypes.JSON, extraData: DataTypes.JSON, - title: DataTypes.STRING, - titleIgnorePrefix: DataTypes.STRING + titleCopy: DataTypes.STRING, + titleIgnorePrefixCopy: DataTypes.STRING }, { sequelize, @@ -705,10 +705,10 @@ class LibraryItem extends Model { fields: ['libraryId', 'mediaType', 'createdAt'] }, { - fields: ['libraryId', 'mediaType', { name: 'title', collate: 'NOCASE' }] + fields: ['libraryId', 'mediaType', { name: 'titleCopy', collate: 'NOCASE' }] }, { - fields: ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }] + fields: ['libraryId', 'mediaType', { name: 'titleIgnorePrefixCopy', collate: 'NOCASE' }] }, { fields: ['libraryId', 'mediaId', 'mediaType'] diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 210f20f9e..543cd5798 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -521,8 +521,8 @@ class BookScanner { libraryItemObj.isMissing = false libraryItemObj.isInvalid = false libraryItemObj.extraData = {} - libraryItemObj.title = bookMetadata.title - libraryItemObj.titleIgnorePrefix = getTitleIgnorePrefix(bookMetadata.title) + libraryItemObj.titleCopy = bookMetadata.title + libraryItemObj.titleIgnorePrefixCopy = getTitleIgnorePrefix(bookMetadata.title) // Set isSupplementary flag on ebook library files for (const libraryFile of libraryItemObj.libraryFiles) { diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 3adb929e0..cec84d567 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -273,9 +273,9 @@ module.exports = { } if (global.ServerSettings.sortingIgnorePrefix) { - return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]] + return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefixCopy` COLLATE NOCASE'), dir]] } else { - return [[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]] + return [[Sequelize.literal('`libraryItem`.`titleCopy` COLLATE NOCASE'), dir]] } } else if (sortBy === 'sequence') { const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST' @@ -346,7 +346,6 @@ module.exports = { async findAndCountAll(findOptions, limit, offset) { const findOptionsKey = JSON.stringify(findOptions) - Logger.debug(`[LibraryItemsBookFilters] findOptionsKey: ${findOptionsKey}`) findOptions.limit = limit || null findOptions.offset = offset diff --git a/test/server/migrations/v2.19.2-change-ambigous-column-names.test.js b/test/server/migrations/v2.19.2-change-ambigous-column-names.test.js new file mode 100644 index 000000000..0409c8a96 --- /dev/null +++ b/test/server/migrations/v2.19.2-change-ambigous-column-names.test.js @@ -0,0 +1,233 @@ +const chai = require('chai') +const sinon = require('sinon') +const { expect } = chai + +const { DataTypes, Sequelize } = require('sequelize') +const Logger = require('../../../server/Logger') + +const { up, down } = require('../../../server/migrations/v2.19.2-change-ambigous-column-names') + +describe('Migration v2.19.2-change-ambigous-column-names', () => { + let sequelize + let queryInterface + let loggerInfoStub + + beforeEach(async () => { + sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + queryInterface = sequelize.getQueryInterface() + loggerInfoStub = sinon.stub(Logger, 'info') + + await queryInterface.createTable('books', { + id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true }, + title: { type: DataTypes.STRING, allowNull: true }, + titleIgnorePrefix: { type: DataTypes.STRING, allowNull: true } + }) + + await queryInterface.createTable('libraryItems', { + id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true }, + libraryId: { type: DataTypes.INTEGER, allowNull: false }, + mediaType: { type: DataTypes.STRING, allowNull: false }, + mediaId: { type: DataTypes.INTEGER, allowNull: false }, + title: { type: DataTypes.STRING, allowNull: true }, + titleIgnorePrefix: { type: DataTypes.STRING, allowNull: true }, + createdAt: { type: DataTypes.DATE, allowNull: false } + }) + + await queryInterface.bulkInsert('books', [ + { id: 1, title: 'The Book 1', titleIgnorePrefix: 'Book 1, The' }, + { id: 2, title: 'Book 2', titleIgnorePrefix: 'Book 2' } + ]) + + await queryInterface.bulkInsert('libraryItems', [ + { id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, title: 'The Book 1', titleIgnorePrefix: 'Book 1, The', createdAt: '2025-01-01 00:00:00.000 +00:00' }, + { id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, title: 'Book 2', titleIgnorePrefix: 'Book 2', createdAt: '2025-01-02 00:00:00.000 +00:00' } + ]) + + // Add indexes to the libraryItems table + await queryInterface.addIndex('libraryItems', ['libraryId', 'mediaType', 'title']) + await queryInterface.addIndex('libraryItems', ['libraryId', 'mediaType', 'titleIgnorePrefix']) + + // Add triggers to the books table + await queryInterface.sequelize.query(` + CREATE TRIGGER update_library_items_title + AFTER UPDATE OF title ON books + FOR EACH ROW + BEGIN + UPDATE libraryItems + SET title = NEW.title + WHERE libraryItems.mediaId = NEW.id; + END; + `) + + await queryInterface.sequelize.query(` + CREATE TRIGGER update_library_items_title_ignore_prefix + AFTER UPDATE OF titleIgnorePrefix ON books + FOR EACH ROW + BEGIN + UPDATE libraryItems + SET titleIgnorePrefix = NEW.titleIgnorePrefix + WHERE libraryItems.mediaId = NEW.id; + END; + `) + }) + + afterEach(() => { + sinon.restore() + }) + + describe('up', () => { + it('should replace title and titleIgnorePrefix with titleCopy and titleIgnorePrefixCopy columns in libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems') + expect(libraryItems).to.deep.equal([ + { id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, titleCopy: 'The Book 1', titleIgnorePrefixCopy: 'Book 1, The', createdAt: '2025-01-01 00:00:00.000 +00:00' }, + { id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, titleCopy: 'Book 2', titleIgnorePrefixCopy: 'Book 2', createdAt: '2025-01-02 00:00:00.000 +00:00' } + ]) + }) + + it('should add index on titleCopy to libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_copy'`) + expect(count).to.equal(1) + }) + + it('should add trigger to books.title to update libraryItems.titleCopy', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_copy'`) + expect(count).to.equal(1) + }) + + it('should add index on titleIgnorePrefixCopy to libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix_copy'`) + expect(count).to.equal(1) + }) + + it('should add trigger to books.titleIgnorePrefix to update libraryItems.titleIgnorePrefixCopy', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix_copy'`) + expect(count).to.equal(1) + }) + + it('should remove title index from libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title'`) + expect(count).to.equal(0) + }) + + it('should remove titleIgnorePrefix index from libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`) + expect(count).to.equal(0) + }) + + it('should remove title trigger from books', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title'`) + expect(count).to.equal(0) + }) + + it('should remove titleIgnorePrefix trigger from books', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix'`) + expect(count).to.equal(0) + }) + }) + + describe('down', () => { + it('should remove titleCopy and titleIgnorePrefixCopy from libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems') + expect(libraryItems).to.deep.equal([ + { id: 1, libraryId: 1, mediaType: 'book', title: 'The Book 1', titleIgnorePrefix: 'Book 1, The', mediaId: 1, createdAt: '2025-01-01 00:00:00.000 +00:00' }, + { id: 2, libraryId: 2, mediaType: 'book', title: 'Book 2', titleIgnorePrefix: 'Book 2', mediaId: 2, createdAt: '2025-01-02 00:00:00.000 +00:00' } + ]) + }) + + it('should remove titleCopy trigger from books', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_copy'`) + expect(count).to.equal(0) + }) + + it('should remove titleIgnorePrefixCopy trigger from books', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix_copy'`) + expect(count).to.equal(0) + }) + + it('should remove index on titleIgnorePrefixCopy from libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix_copy'`) + expect(count).to.equal(0) + }) + + it('should remove index on titleCopy from libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_copy'`) + expect(count).to.equal(0) + }) + + it('should add back title and titleIgnorePrefix columns to libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems') + expect(libraryItems).to.deep.equal([ + { id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, title: 'The Book 1', titleIgnorePrefix: 'Book 1, The', createdAt: '2025-01-01 00:00:00.000 +00:00' }, + { id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, title: 'Book 2', titleIgnorePrefix: 'Book 2', createdAt: '2025-01-02 00:00:00.000 +00:00' } + ]) + }) + + it('should add back title trigger to books', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title'`) + expect(count).to.equal(1) + }) + + it('should add back titleIgnorePrefix trigger to books', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix'`) + expect(count).to.equal(1) + }) + + it('should add back title index to libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title'`) + expect(count).to.equal(1) + }) + + it('should add back titleIgnorePrefix index to libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`) + expect(count).to.equal(1) + }) + }) +})