Index moderation case references for database integrity checks
CI / checks (push) Successful in 7m31s
CI / checks (push) Successful in 7m31s
This commit is contained in:
@@ -34,7 +34,8 @@ import {
|
|||||||
migrateModerationCaseActions,
|
migrateModerationCaseActions,
|
||||||
migrateModerationCaseAssessments,
|
migrateModerationCaseAssessments,
|
||||||
migrateModerationCaseRelationships,
|
migrateModerationCaseRelationships,
|
||||||
migrateModerationCaseFollowups
|
migrateModerationCaseFollowups,
|
||||||
|
migrateModerationCaseReferenceIndexes
|
||||||
} from '../moderation/schema.js';
|
} from '../moderation/schema.js';
|
||||||
import type { Migration } from './migration-runner.js';
|
import type { Migration } from './migration-runner.js';
|
||||||
|
|
||||||
@@ -197,6 +198,11 @@ export const migrations: readonly Migration[] = [
|
|||||||
version: 34,
|
version: 34,
|
||||||
name: 'moderation follow-up dates and personal updates',
|
name: 'moderation follow-up dates and personal updates',
|
||||||
run: migrateModerationCaseFollowups
|
run: migrateModerationCaseFollowups
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 35,
|
||||||
|
name: 'moderation case foreign key reference indexes',
|
||||||
|
run: migrateModerationCaseReferenceIndexes
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -223,3 +223,15 @@ export function migrateModerationCaseFollowups(handle: Database.Database): void
|
|||||||
CREATE INDEX IF NOT EXISTS moderation_case_notifications_case ON moderation_case_notifications(user_id,case_id,read_at,id DESC);
|
CREATE INDEX IF NOT EXISTS moderation_case_notifications_case ON moderation_case_notifications(user_id,case_id,read_at,id DESC);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Foreign-key lookups must cover removed relationships and every notification recipient. */
|
||||||
|
export function migrateModerationCaseReferenceIndexes(handle: Database.Database): void {
|
||||||
|
handle.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS moderation_case_review_sanction
|
||||||
|
ON moderation_cases(review_sanction_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS moderation_case_relationship_source
|
||||||
|
ON moderation_case_relationships(case_id,removed_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS moderation_case_notifications_parent
|
||||||
|
ON moderation_case_notifications(case_id);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||||
|
|
||||||
let fixture: Fixture;
|
let fixture: Fixture;
|
||||||
@@ -10,7 +11,30 @@ afterEach(async () => {
|
|||||||
await fixture.cleanup();
|
await fixture.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('indexes campaign cascades and beta evidence on fresh and reopened databases', async () => {
|
function expectModerationReferenceIndexes(handle: Database.Database) {
|
||||||
|
for (const [query, index] of [
|
||||||
|
[
|
||||||
|
"SELECT id FROM moderation_cases WHERE review_sanction_id='restriction'",
|
||||||
|
'moderation_case_review_sanction'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"SELECT id FROM moderation_case_relationships WHERE case_id='source'",
|
||||||
|
'moderation_case_relationship_source'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"SELECT id FROM moderation_case_notifications WHERE case_id='source'",
|
||||||
|
'moderation_case_notifications_parent'
|
||||||
|
]
|
||||||
|
]) {
|
||||||
|
const plan = handle.prepare(`EXPLAIN QUERY PLAN ${query}`).all() as { detail: string }[];
|
||||||
|
expect(
|
||||||
|
plan.some((row) => row.detail.includes(index)),
|
||||||
|
query
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('indexes campaign, beta and moderation references on fresh and reopened databases', async () => {
|
||||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||||
for (let pass = 0; pass < 2; pass++) {
|
for (let pass = 0; pass < 2; pass++) {
|
||||||
const handle = db();
|
const handle = db();
|
||||||
@@ -29,11 +53,48 @@ it('indexes campaign cascades and beta evidence on fresh and reopened databases'
|
|||||||
}[];
|
}[];
|
||||||
expect(plan.some((row) => row.detail.includes(index))).toBe(true);
|
expect(plan.some((row) => row.detail.includes(index))).toBe(true);
|
||||||
}
|
}
|
||||||
|
expectModerationReferenceIndexes(handle);
|
||||||
expect(handle.pragma('foreign_key_check')).toEqual([]);
|
expect(handle.pragma('foreign_key_check')).toEqual([]);
|
||||||
closeDb();
|
closeDb();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds complete moderation reference indexes when upgrading version 34 without changing records', async () => {
|
||||||
|
const { migrations } = await import('../../src/lib/server/db/migrations.js');
|
||||||
|
const { runMigrations } = await import('../../src/lib/server/db/migration-runner.js');
|
||||||
|
const legacy = new Database(':memory:');
|
||||||
|
legacy.pragma('foreign_keys = ON');
|
||||||
|
try {
|
||||||
|
runMigrations(legacy, migrations.slice(0, 34));
|
||||||
|
legacy.exec(`
|
||||||
|
INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at)
|
||||||
|
VALUES('moderator','moderator','Moderator','unused','moderator',0,0);
|
||||||
|
INSERT INTO moderation_cases(id,game,guid,title,status,actor_id,actor,created_at,updated_at)
|
||||||
|
VALUES('source','t5','123','Source case','open','moderator','Moderator','2026-10-17','2026-10-17'),
|
||||||
|
('target','t5','123','Target case','open','moderator','Moderator','2026-10-17','2026-10-17');
|
||||||
|
INSERT INTO moderation_sanctions(id,game,guid,kind,scope,reason,note,actor_id,actor,created_at)
|
||||||
|
VALUES('restriction','t5','123','mute','game','Recorded restriction','','moderator','Moderator','2026-10-17');
|
||||||
|
UPDATE moderation_cases SET review_sanction_id='restriction' WHERE id='source';
|
||||||
|
INSERT INTO moderation_case_relationships VALUES('relationship','source','target','same_incident','moderator','Moderator','2026-10-17','2026-10-18');
|
||||||
|
INSERT INTO moderation_case_notifications(user_id,case_id,kind,created_at)
|
||||||
|
VALUES('moderator','source','evidence','2026-10-17');
|
||||||
|
`);
|
||||||
|
const records = () => ({
|
||||||
|
cases: legacy.prepare('SELECT * FROM moderation_cases ORDER BY id').all(),
|
||||||
|
relationships: legacy.prepare('SELECT * FROM moderation_case_relationships').all(),
|
||||||
|
notifications: legacy.prepare('SELECT * FROM moderation_case_notifications').all()
|
||||||
|
});
|
||||||
|
const before = records();
|
||||||
|
runMigrations(legacy, migrations);
|
||||||
|
runMigrations(legacy, migrations);
|
||||||
|
expectModerationReferenceIndexes(legacy);
|
||||||
|
expect(records()).toEqual(before);
|
||||||
|
expect(legacy.pragma('foreign_key_check')).toEqual([]);
|
||||||
|
} finally {
|
||||||
|
legacy.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('covers every foreign key child prefix, including host and library schemas', async () => {
|
it('covers every foreign key child prefix, including host and library schemas', async () => {
|
||||||
const { db } = await import('../../src/lib/server/db/index.js');
|
const { db } = await import('../../src/lib/server/db/index.js');
|
||||||
const handle = db();
|
const handle = db();
|
||||||
|
|||||||
Reference in New Issue
Block a user