Testing Migrations

This commit is contained in:
Max committed 2026-11-03 12:50:32 -07:00
1 parent 81b4162117
commit d9abeb7fb1
7 files changed
+105 -78

No files matched your search

+31 -2
View File
@@ -10,7 +10,7 @@
//
// The restricted account is correct - the server ingests untrusted URLs and
// should not be able to reshape its own tables. So DDL moved out of the
// application entirely: an admin runs ./applyScrapeSchema.sh once, and this
// application entirely: an admin runs ./migrateDatabase.sh once, and this
// module only checks that the result is present and usable.
//
// Note on why the DDL is REMOVED rather than guarded by an existence check:
@@ -27,6 +27,17 @@ const REQUIRED_TABLE = 'scrape_cache'
const REQUIRED_COLUMNS = ['scrape_state', 'scrape_attempts', 'enqueued_at']
const REQUIRED_INDEX = 'attachment_scrape_state'
//
// The FULLTEXT index backing Attachment.textSearch's MATCH..AGAINST.
//
// Checked by TYPE, not just existence: Prisma's `@@fulltext` is a preview
// feature and MariaDB is not among its documented databases, so a future
// migration could plausibly emit a plain BTREE index of the same name. That
// would make attachment search return nothing at all, with no error anywhere -
// so it is worth asserting the type explicitly on every boot.
//
const REQUIRED_FULLTEXT = { table: 'attachment', index: 'attachment_text_index' }
// The scraper writes and deletes, not just reads. DELETE is genuinely needed:
// terminal failures drop the shell attachment row (ScrapeQueue), and
// ScrapeCache.purgeExpired removes expired rows.
@@ -62,6 +73,19 @@ const columnExists = async (table, column) => {
return rows.length > 0
}
//
// Does this index exist AND is it actually FULLTEXT?
//
const fulltextIndexExists = async (table, index) => {
const [rows] = await db.promise().query(
`SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?
AND INDEX_TYPE = 'FULLTEXT' LIMIT 1`,
[table, index]
)
return rows.length > 0
}
const indexExists = async (table, index) => {
const [rows] = await db.promise().query(
`SELECT 1 FROM information_schema.STATISTICS
@@ -74,6 +98,7 @@ const indexExists = async (table, index) => {
Schema.__tableExists = tableExists
Schema.__columnExists = columnExists
Schema.__indexExists = indexExists
Schema.__fulltextIndexExists = fulltextIndexExists
//
// Parse SHOW GRANTS output into the set of privileges that apply to this
@@ -182,12 +207,16 @@ Schema.verifyScrapeSchema = async () => {
missingObjects.push(`attachment index ${REQUIRED_INDEX}`)
}
if (!await fulltextIndexExists(REQUIRED_FULLTEXT.table, REQUIRED_FULLTEXT.index)) {
missingObjects.push(`FULLTEXT index ${REQUIRED_FULLTEXT.table}.${REQUIRED_FULLTEXT.index}`)
}
const privileges = await checkPrivileges()
const missingPrivileges = privileges.missing || []
const remedy = []
if (missingObjects.length) {
remedy.push('run ./applyScrapeSchema.sh as a database admin')
remedy.push('run ./migrateDatabase.sh as a database admin')
}
if (missingPrivileges.length) {
// Name the real database and user so this can be pasted straight into