mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-08-03 15:11:44 +00:00
The sqlite triggers that keep libraryItems.title[IgnorePrefix] and authorNamesFirstLast/LastFirst fresh were skipped on postgres, so title edits, author renames, and bookAuthor changes left stale sort/search columns. Mirror them with native pg trigger functions (string_agg in place of GROUP_CONCAT) using folded lowercase identifiers, and keep the same trigger names and existence checks. Validated against PostgreSQL 14: title propagation, author insert/delete/rename, createdat ordering, and null-on-empty all match sqlite semantics.
160 lines
5.4 KiB
JavaScript
160 lines
5.4 KiB
JavaScript
const { expect } = require('chai')
|
|
const sinon = require('sinon')
|
|
|
|
const fs = require('../../server/libs/fsExtra')
|
|
const Database = require('../../server/Database')
|
|
|
|
describe('Database', () => {
|
|
let originalDialect
|
|
let originalSequelize
|
|
let originalEnv
|
|
|
|
beforeEach(() => {
|
|
originalDialect = Database.dialect
|
|
originalSequelize = Database.sequelize
|
|
originalEnv = {
|
|
DB_DIALECT: process.env.DB_DIALECT,
|
|
DATABASE_URL: process.env.DATABASE_URL
|
|
}
|
|
})
|
|
|
|
afterEach(() => {
|
|
Database.dialect = originalDialect
|
|
Database.sequelize = originalSequelize
|
|
|
|
if (originalEnv.DB_DIALECT === undefined) delete process.env.DB_DIALECT
|
|
else process.env.DB_DIALECT = originalEnv.DB_DIALECT
|
|
|
|
if (originalEnv.DATABASE_URL === undefined) delete process.env.DATABASE_URL
|
|
else process.env.DATABASE_URL = originalEnv.DATABASE_URL
|
|
|
|
sinon.restore()
|
|
})
|
|
|
|
describe('getConfiguredDialect', () => {
|
|
it('should default to sqlite when no env variables are set', () => {
|
|
delete process.env.DB_DIALECT
|
|
delete process.env.DATABASE_URL
|
|
|
|
expect(Database.getConfiguredDialect()).to.equal('sqlite')
|
|
})
|
|
|
|
it('should use explicit DB_DIALECT value when valid', () => {
|
|
process.env.DB_DIALECT = 'postgres'
|
|
process.env.DATABASE_URL = 'sqlite:///tmp/abs.sqlite'
|
|
|
|
expect(Database.getConfiguredDialect()).to.equal('postgres')
|
|
})
|
|
|
|
it('should infer postgres dialect from DATABASE_URL', () => {
|
|
delete process.env.DB_DIALECT
|
|
process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/abs'
|
|
|
|
expect(Database.getConfiguredDialect()).to.equal('postgres')
|
|
})
|
|
|
|
it('should fallback to sqlite for unsupported DB_DIALECT', () => {
|
|
process.env.DB_DIALECT = 'mysql'
|
|
process.env.DATABASE_URL = 'sqlite:///tmp/abs.sqlite'
|
|
|
|
expect(Database.getConfiguredDialect()).to.equal('sqlite')
|
|
})
|
|
})
|
|
|
|
describe('checkHasDb', () => {
|
|
it('should not check sqlite file existence in postgres mode', async () => {
|
|
Database.dialect = 'postgres'
|
|
const pathExistsStub = sinon.stub(fs, 'pathExists')
|
|
|
|
const hasDb = await Database.checkHasDb()
|
|
|
|
expect(hasDb).to.equal(true)
|
|
expect(pathExistsStub.called).to.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('checkHasTables', () => {
|
|
it('should return true when at least one table exists', async () => {
|
|
Database.sequelize = {
|
|
getQueryInterface: () => ({
|
|
showAllTables: sinon.stub().resolves(['users'])
|
|
})
|
|
}
|
|
|
|
const hasTables = await Database.checkHasTables()
|
|
|
|
expect(hasTables).to.equal(true)
|
|
})
|
|
|
|
it('should return false when no tables exist', async () => {
|
|
Database.sequelize = {
|
|
getQueryInterface: () => ({
|
|
showAllTables: sinon.stub().resolves([])
|
|
})
|
|
}
|
|
|
|
const hasTables = await Database.checkHasTables()
|
|
|
|
expect(hasTables).to.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('addPostgresTriggers', () => {
|
|
function captureQueries(existingTriggers = []) {
|
|
const queries = []
|
|
Database.sequelize = {
|
|
query: async (sql) => {
|
|
queries.push(sql)
|
|
const count = existingTriggers.filter((name) => sql.includes(`tgname = '${name}'`)).length
|
|
return [[{ count }]]
|
|
}
|
|
}
|
|
return queries
|
|
}
|
|
|
|
it('should create title and author names triggers with folded lowercase identifiers', async () => {
|
|
const queries = captureQueries()
|
|
|
|
await Database.addPostgresTriggers()
|
|
|
|
const functions = queries.filter((sql) => sql.includes('CREATE OR REPLACE FUNCTION'))
|
|
const triggers = queries.filter((sql) => sql.includes('CREATE TRIGGER'))
|
|
expect(functions.length).to.equal(7)
|
|
expect(triggers.length).to.equal(7)
|
|
|
|
const allDdl = [...functions, ...triggers].join('\n')
|
|
// No camelCase identifiers may leak into postgres DDL - unquoted identifiers fold to lowercase
|
|
expect(allDdl).to.not.match(/libraryItems|bookAuthors|mediaId|titleIgnorePrefix|authorNames|bookId|authorId|lastFirst|createdAt/)
|
|
|
|
const authorTrigger = triggers.find((sql) => sql.includes('update_library_items_author_names_on_authors_update'))
|
|
expect(authorTrigger).to.include('AFTER UPDATE OF name ON authors')
|
|
|
|
const insertFn = functions.find((sql) => sql.includes('update_library_items_author_names_on_book_authors_insert_fn'))
|
|
expect(insertFn).to.include("string_agg(authors.name, ', ' ORDER BY bookauthors.createdat ASC)")
|
|
expect(insertFn).to.include('WHERE mediaid = NEW.bookid')
|
|
|
|
const deleteFn = functions.find((sql) => sql.includes('update_library_items_author_names_on_book_authors_delete_fn'))
|
|
expect(deleteFn).to.include('WHERE mediaid = OLD.bookid')
|
|
})
|
|
|
|
it('should skip triggers that already exist', async () => {
|
|
const queries = captureQueries(['update_library_items_title'])
|
|
|
|
await Database.addPostgresTriggers()
|
|
|
|
const titleFn = queries.find((sql) => sql.includes('update_library_items_title_fn'))
|
|
expect(titleFn).to.equal(undefined)
|
|
const otherFns = queries.filter((sql) => sql.includes('CREATE OR REPLACE FUNCTION'))
|
|
expect(otherFns.length).to.equal(6)
|
|
})
|
|
|
|
it('should dispatch to postgres triggers from addTriggers', async () => {
|
|
Database.dialect = 'postgres'
|
|
const queries = captureQueries()
|
|
|
|
await Database.addTriggers()
|
|
|
|
expect(queries.some((sql) => sql.includes('CREATE TRIGGER'))).to.equal(true)
|
|
})
|
|
})
|
|
})
|