mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-08-03 07:01:56 +00:00
make v2.33.0 migration index checks work on postgres
Sequelize showIndex matches the table name case-sensitively, but postgres folds unquoted identifiers to lowercase, so existing indexes were never detected and addIndex failed with already-exists errors, crash-looping the server on startup. Query pg_indexes with the folded table name on postgres and compare index names case-insensitively.
This commit is contained in:
parent
3f2919fce5
commit
49d0a66c1d
2 changed files with 139 additions and 4 deletions
|
|
@ -45,8 +45,8 @@ async function down({ context: { queryInterface, logger } }) {
|
|||
}
|
||||
|
||||
async function addIndexIfMissing(queryInterface, logger, index) {
|
||||
const existing = await queryInterface.showIndex(index.table)
|
||||
if (existing.some((i) => i.name === index.name)) {
|
||||
const existing = await showIndexNames(queryInterface, index.table)
|
||||
if (existing.some((name) => name.toLowerCase() === index.name.toLowerCase())) {
|
||||
logger.info(`${loggerPrefix} index ${index.name} already exists on ${index.table}`)
|
||||
return
|
||||
}
|
||||
|
|
@ -60,8 +60,8 @@ async function addIndexIfMissing(queryInterface, logger, index) {
|
|||
}
|
||||
|
||||
async function removeIndexIfExists(queryInterface, logger, index) {
|
||||
const existing = await queryInterface.showIndex(index.table)
|
||||
if (!existing.some((i) => i.name === index.name)) {
|
||||
const existing = await showIndexNames(queryInterface, index.table)
|
||||
if (!existing.some((name) => name.toLowerCase() === index.name.toLowerCase())) {
|
||||
logger.info(`${loggerPrefix} index ${index.name} does not exist on ${index.table}`)
|
||||
return
|
||||
}
|
||||
|
|
@ -71,4 +71,21 @@ async function removeIndexIfExists(queryInterface, logger, index) {
|
|||
logger.info(`${loggerPrefix} removed index ${index.name}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sequelize showIndex matches the table name case-sensitively, but postgres folds
|
||||
* unquoted identifiers to lowercase, so query pg_indexes directly on postgres.
|
||||
*
|
||||
* @returns {Promise<string[]>} index names on the table
|
||||
*/
|
||||
async function showIndexNames(queryInterface, table) {
|
||||
if (queryInterface.sequelize.getDialect() === 'postgres') {
|
||||
const [rows] = await queryInterface.sequelize.query('SELECT indexname AS name FROM pg_indexes WHERE schemaname = current_schema() AND tablename = $1', {
|
||||
bind: [table.toLowerCase()]
|
||||
})
|
||||
return rows.map((row) => row.name)
|
||||
}
|
||||
const existing = await queryInterface.showIndex(table)
|
||||
return existing.map((row) => row.name)
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const { up, down } = require('../../../server/migrations/v2.33.0-add-discover-query-indexes')
|
||||
const { Sequelize } = require('sequelize')
|
||||
const Logger = require('../../../server/Logger')
|
||||
|
||||
describe('migration-v2.33.0-add-discover-query-indexes', () => {
|
||||
let sequelize
|
||||
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 () => {
|
||||
await queryInterface.createTable('mediaProgresses', {
|
||||
id: { type: Sequelize.UUID, primaryKey: true },
|
||||
userId: { type: Sequelize.UUID, allowNull: false },
|
||||
mediaItemId: { type: Sequelize.UUID, allowNull: false },
|
||||
isFinished: { type: Sequelize.BOOLEAN, allowNull: false },
|
||||
currentTime: { type: Sequelize.FLOAT, allowNull: false }
|
||||
})
|
||||
await queryInterface.createTable('bookSeries', {
|
||||
id: { type: Sequelize.UUID, primaryKey: true },
|
||||
seriesId: { type: Sequelize.UUID, allowNull: false },
|
||||
bookId: { type: Sequelize.UUID, allowNull: false }
|
||||
})
|
||||
})
|
||||
|
||||
it('should add both discover query indexes', async () => {
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
const mediaProgressIndexes = await queryInterface.showIndex('mediaProgresses')
|
||||
expect(mediaProgressIndexes.some((i) => i.name === 'media_progresses_user_item_finished_time')).to.equal(true)
|
||||
const bookSeriesIndexes = await queryInterface.showIndex('bookSeries')
|
||||
expect(bookSeriesIndexes.some((i) => i.name === 'book_series_series_book')).to.equal(true)
|
||||
})
|
||||
|
||||
it('should not fail when the indexes already exist', async () => {
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.calledWithMatch('index media_progresses_user_item_finished_time already exists')).to.equal(true)
|
||||
expect(loggerInfoStub.calledWithMatch('index book_series_series_book already exists')).to.equal(true)
|
||||
})
|
||||
|
||||
it('should detect existing indexes on postgres via pg_indexes with folded table names', async () => {
|
||||
// Sequelize showIndex matches relname case-sensitively and misses folded
|
||||
// lowercase postgres tables, so the migration must query pg_indexes instead
|
||||
const queries = []
|
||||
const fakeQueryInterface = {
|
||||
sequelize: {
|
||||
getDialect: () => 'postgres',
|
||||
query: async (sql, options) => {
|
||||
queries.push({ sql, options })
|
||||
return [[{ name: 'media_progresses_user_item_finished_time' }, { name: 'book_series_series_book' }]]
|
||||
}
|
||||
},
|
||||
addIndex: async () => {
|
||||
throw new Error('addIndex must not be called for existing indexes')
|
||||
},
|
||||
showIndex: async () => {
|
||||
throw new Error('showIndex must not be used on postgres')
|
||||
}
|
||||
}
|
||||
|
||||
await up({ context: { queryInterface: fakeQueryInterface, logger: Logger } })
|
||||
|
||||
expect(queries.length).to.be.greaterThan(0)
|
||||
expect(queries.every((q) => q.sql.includes('pg_indexes'))).to.equal(true)
|
||||
expect(queries.map((q) => q.options.bind[0])).to.include.members(['mediaprogresses', 'bookseries'])
|
||||
expect(loggerInfoStub.calledWithMatch('index media_progresses_user_item_finished_time already exists')).to.equal(true)
|
||||
expect(loggerInfoStub.calledWithMatch('index book_series_series_book already exists')).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('down', () => {
|
||||
beforeEach(async () => {
|
||||
await queryInterface.createTable('mediaProgresses', {
|
||||
id: { type: Sequelize.UUID, primaryKey: true },
|
||||
userId: { type: Sequelize.UUID, allowNull: false },
|
||||
mediaItemId: { type: Sequelize.UUID, allowNull: false },
|
||||
isFinished: { type: Sequelize.BOOLEAN, allowNull: false },
|
||||
currentTime: { type: Sequelize.FLOAT, allowNull: false }
|
||||
})
|
||||
await queryInterface.createTable('bookSeries', {
|
||||
id: { type: Sequelize.UUID, primaryKey: true },
|
||||
seriesId: { type: Sequelize.UUID, allowNull: false },
|
||||
bookId: { type: Sequelize.UUID, allowNull: false }
|
||||
})
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
})
|
||||
|
||||
it('should remove both discover query indexes', async () => {
|
||||
await down({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
const mediaProgressIndexes = await queryInterface.showIndex('mediaProgresses')
|
||||
expect(mediaProgressIndexes.some((i) => i.name === 'media_progresses_user_item_finished_time')).to.equal(false)
|
||||
const bookSeriesIndexes = await queryInterface.showIndex('bookSeries')
|
||||
expect(bookSeriesIndexes.some((i) => i.name === 'book_series_series_book')).to.equal(false)
|
||||
})
|
||||
|
||||
it('should not fail when the indexes do not exist', async () => {
|
||||
await down({ context: { queryInterface, logger: Logger } })
|
||||
await down({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.calledWithMatch('index media_progresses_user_item_finished_time does not exist')).to.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue