mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	* Add REINDEX NOCASE migration and update v2.15.0 migration * Update v2.15.0 migration test * Fix typo
		
			
				
	
	
		
			211 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			211 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * @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.
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * This upward migration script cleans any duplicate series in the `Series` table and
 | 
						|
 * adds a unique index on the `name` and `libraryId` columns.
 | 
						|
 *
 | 
						|
 * @param {MigrationOptions} options - an object containing the migration context.
 | 
						|
 * @returns {Promise<void>} - A promise that resolves when the migration is complete.
 | 
						|
 */
 | 
						|
async function up({ context: { queryInterface, logger } }) {
 | 
						|
  // Upwards migration script
 | 
						|
  logger.info('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique ')
 | 
						|
 | 
						|
  // Run reindex nocase to fix potential corruption issues due to the bad sqlite extension introduced in v2.12.0
 | 
						|
  logger.info('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues')
 | 
						|
  await queryInterface.sequelize.query('REINDEX NOCASE;')
 | 
						|
 | 
						|
  // Check if the unique index already exists
 | 
						|
  const seriesIndexes = await queryInterface.showIndex('Series')
 | 
						|
  if (seriesIndexes.some((index) => index.name === 'unique_series_name_per_library')) {
 | 
						|
    logger.info('[2.15.0 migration] Unique index on Series.name and Series.libraryId already exists')
 | 
						|
    logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ')
 | 
						|
    return
 | 
						|
  }
 | 
						|
 | 
						|
  // The steps taken to deduplicate the series are as follows:
 | 
						|
  // 1. Find all duplicate series in the `Series` table.
 | 
						|
  // 2. Iterate over the duplicate series and find all book IDs that are associated with the duplicate series in `bookSeries` table.
 | 
						|
  //    2.a For each book ID, check if the ID occurs multiple times for the duplicate series.
 | 
						|
  //    2.b If so, keep only one of the rows that has this bookId and seriesId.
 | 
						|
  // 3. Update `bookSeries` table to point to the most recent series.
 | 
						|
  // 4. Delete the older series.
 | 
						|
 | 
						|
  // Use the queryInterface to get the series table and find duplicates in the `name` and `libraryId` column
 | 
						|
  const [duplicates] = await queryInterface.sequelize.query(`
 | 
						|
    SELECT name, libraryId
 | 
						|
    FROM Series
 | 
						|
    GROUP BY name, libraryId
 | 
						|
    HAVING COUNT(name) > 1
 | 
						|
  `)
 | 
						|
 | 
						|
  // Print out how many duplicates were found
 | 
						|
  logger.info(`[2.15.0 migration] Found ${duplicates.length} duplicate series`)
 | 
						|
 | 
						|
  // Iterate over each duplicate series
 | 
						|
  for (const duplicate of duplicates) {
 | 
						|
    // Report the series name that is being deleted
 | 
						|
    logger.info(`[2.15.0 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`)
 | 
						|
 | 
						|
    // Determine any duplicate book IDs in the `bookSeries` table for the same series
 | 
						|
    const [duplicateBookIds] = await queryInterface.sequelize.query(
 | 
						|
      `
 | 
						|
        SELECT bookId
 | 
						|
        FROM BookSeries
 | 
						|
        WHERE seriesId IN (
 | 
						|
          SELECT id
 | 
						|
          FROM Series
 | 
						|
          WHERE name = :name AND libraryId = :libraryId
 | 
						|
        )
 | 
						|
        GROUP BY bookId
 | 
						|
        HAVING COUNT(bookId) > 1
 | 
						|
        `,
 | 
						|
      {
 | 
						|
        replacements: {
 | 
						|
          name: duplicate.name,
 | 
						|
          libraryId: duplicate.libraryId
 | 
						|
        }
 | 
						|
      }
 | 
						|
    )
 | 
						|
 | 
						|
    // Iterate over the duplicate book IDs if there is at least one and only keep the first row that has this bookId and seriesId
 | 
						|
    for (const { bookId } of duplicateBookIds) {
 | 
						|
      logger.info(`[2.15.0 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`)
 | 
						|
      // Get all rows of `BookSeries` table that have the same `bookId` and `seriesId`. Sort by `sequence` with nulls sorted last
 | 
						|
      const [duplicateBookSeries] = await queryInterface.sequelize.query(
 | 
						|
        `
 | 
						|
            SELECT id
 | 
						|
            FROM BookSeries
 | 
						|
            WHERE bookId = :bookId
 | 
						|
            AND seriesId IN (
 | 
						|
              SELECT id
 | 
						|
              FROM Series
 | 
						|
              WHERE name = :name AND libraryId = :libraryId
 | 
						|
            )
 | 
						|
            ORDER BY sequence NULLS LAST
 | 
						|
            `,
 | 
						|
        {
 | 
						|
          replacements: {
 | 
						|
            bookId,
 | 
						|
            name: duplicate.name,
 | 
						|
            libraryId: duplicate.libraryId
 | 
						|
          }
 | 
						|
        }
 | 
						|
      )
 | 
						|
 | 
						|
      // remove the first element from the array
 | 
						|
      duplicateBookSeries.shift()
 | 
						|
 | 
						|
      // Delete the remaining duplicate rows
 | 
						|
      if (duplicateBookSeries.length > 0) {
 | 
						|
        const [deletedBookSeries] = await queryInterface.sequelize.query(
 | 
						|
          `
 | 
						|
              DELETE FROM BookSeries
 | 
						|
              WHERE id IN (:ids)
 | 
						|
              `,
 | 
						|
          {
 | 
						|
            replacements: {
 | 
						|
              ids: duplicateBookSeries.map((row) => row.id)
 | 
						|
            }
 | 
						|
          }
 | 
						|
        )
 | 
						|
      }
 | 
						|
      logger.info(`[2.15.0 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`)
 | 
						|
    }
 | 
						|
 | 
						|
    // Get all the most recent series which matches the `name` and `libraryId`
 | 
						|
    const [mostRecentSeries] = await queryInterface.sequelize.query(
 | 
						|
      `
 | 
						|
        SELECT id
 | 
						|
        FROM Series
 | 
						|
        WHERE name = :name AND libraryId = :libraryId
 | 
						|
        ORDER BY updatedAt DESC
 | 
						|
        LIMIT 1
 | 
						|
        `,
 | 
						|
      {
 | 
						|
        replacements: {
 | 
						|
          name: duplicate.name,
 | 
						|
          libraryId: duplicate.libraryId
 | 
						|
        },
 | 
						|
        type: queryInterface.sequelize.QueryTypes.SELECT
 | 
						|
      }
 | 
						|
    )
 | 
						|
 | 
						|
    if (mostRecentSeries) {
 | 
						|
      // Update all BookSeries records for this series to point to the most recent series
 | 
						|
      const [seriesUpdated] = await queryInterface.sequelize.query(
 | 
						|
        `
 | 
						|
          UPDATE BookSeries
 | 
						|
          SET seriesId = :mostRecentSeriesId
 | 
						|
          WHERE seriesId IN (
 | 
						|
            SELECT id
 | 
						|
            FROM Series
 | 
						|
            WHERE name = :name AND libraryId = :libraryId
 | 
						|
            AND id != :mostRecentSeriesId
 | 
						|
          )
 | 
						|
          `,
 | 
						|
        {
 | 
						|
          replacements: {
 | 
						|
            name: duplicate.name,
 | 
						|
            libraryId: duplicate.libraryId,
 | 
						|
            mostRecentSeriesId: mostRecentSeries.id
 | 
						|
          }
 | 
						|
        }
 | 
						|
      )
 | 
						|
 | 
						|
      // Delete the older series
 | 
						|
      const seriesDeleted = await queryInterface.sequelize.query(
 | 
						|
        `
 | 
						|
          DELETE FROM Series
 | 
						|
          WHERE name = :name AND libraryId = :libraryId
 | 
						|
          AND id != :mostRecentSeriesId
 | 
						|
          `,
 | 
						|
        {
 | 
						|
          replacements: {
 | 
						|
            name: duplicate.name,
 | 
						|
            libraryId: duplicate.libraryId,
 | 
						|
            mostRecentSeriesId: mostRecentSeries.id
 | 
						|
          }
 | 
						|
        }
 | 
						|
      )
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  logger.info(`[2.15.0 migration] Deduplication complete`)
 | 
						|
 | 
						|
  // Create a unique index based on the name and library ID for the `Series` table
 | 
						|
  await queryInterface.addIndex('Series', ['name', 'libraryId'], {
 | 
						|
    unique: true,
 | 
						|
    name: 'unique_series_name_per_library'
 | 
						|
  })
 | 
						|
  logger.info('[2.15.0 migration] Added unique index on Series.name and Series.libraryId')
 | 
						|
 | 
						|
  logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ')
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * This removes the unique index on the `Series` table.
 | 
						|
 *
 | 
						|
 * @param {MigrationOptions} options - an object containing the migration context.
 | 
						|
 * @returns {Promise<void>} - A promise that resolves when the migration is complete.
 | 
						|
 */
 | 
						|
async function down({ context: { queryInterface, logger } }) {
 | 
						|
  // Downward migration script
 | 
						|
  logger.info('[2.15.0 migration] DOWNGRADE BEGIN: 2.15.0-series-column-unique ')
 | 
						|
 | 
						|
  // Remove the unique index
 | 
						|
  await queryInterface.removeIndex('Series', 'unique_series_name_per_library')
 | 
						|
  logger.info('[2.15.0 migration] Removed unique index on Series.name and Series.libraryId')
 | 
						|
 | 
						|
  logger.info('[2.15.0 migration] DOWNGRADE END: 2.15.0-series-column-unique ')
 | 
						|
}
 | 
						|
 | 
						|
module.exports = { up, down }
 |