Files
electric-cherry/tests/unit/database-indexes.test.ts
2026-10-17 20:14:48 -04:00

130 lines
5.6 KiB
TypeScript

import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import Database from 'better-sqlite3';
import { newFixture, type Fixture } from '../helpers/fixture.js';
let fixture: Fixture;
beforeEach(async () => {
vi.resetModules();
fixture = await newFixture([]);
});
afterEach(async () => {
await fixture.cleanup();
});
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');
for (let pass = 0; pass < 2; pass++) {
const handle = db();
for (const [query, index] of [
[
'SELECT id FROM staged_file_changes WHERE deployment_file_id = 1',
'idx_staged_files_deployment_file'
],
[
'SELECT id FROM beta_test_runs WHERE beta_id = 1 AND revision = 2',
'idx_beta_test_runs_beta_revision'
]
]) {
const plan = handle.prepare(`EXPLAIN QUERY PLAN ${query}`).all() as {
detail: string;
}[];
expect(plan.some((row) => row.detail.includes(index))).toBe(true);
}
expectModerationReferenceIndexes(handle);
expect(handle.pragma('foreign_key_check')).toEqual([]);
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 () => {
const { db } = await import('../../src/lib/server/db/index.js');
const handle = db();
const tables = handle.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as {
name: string;
}[];
const missing: string[] = [];
for (const { name } of tables) {
const foreignKeys = handle.pragma(`foreign_key_list(${name})`) as {
from: string;
seq: number;
}[];
const indexes = handle.pragma(`index_list(${name})`) as { name: string; partial: number }[];
const prefixes = indexes
.filter((index) => !index.partial)
.map(
(index) =>
(handle.pragma(`index_info(${index.name})`) as { name: string }[])[0]?.name
);
const columns = handle.pragma(`table_info(${name})`) as {
name: string;
type: string;
pk: number;
}[];
if (columns.filter((column) => column.pk).length === 1)
prefixes.push(columns.find((column) => column.pk)!.name);
for (const key of foreignKeys.filter((key) => key.seq === 0)) {
if (!prefixes.includes(key.from)) missing.push(`${name}.${key.from}`);
}
}
expect(missing).toEqual([]);
});