Files
pn-new-crm/tests/unit/query-plans.test.ts

120 lines
4.4 KiB
TypeScript
Raw Normal View History

import { describe, it, expect } from 'vitest';
// Document the 10 most common queries and their expected execution plans
const CRITICAL_QUERIES = [
{
name: 'Client list (paginated, port-scoped)',
sql: `SELECT * FROM clients WHERE port_id = $1 AND archived_at IS NULL ORDER BY updated_at DESC LIMIT $2 OFFSET $3`,
expectedIndex: 'idx_clients_port',
maxRows: 1000,
},
{
name: 'Interest list (paginated, port-scoped)',
sql: `SELECT * FROM interests WHERE port_id = $1 AND archived_at IS NULL ORDER BY updated_at DESC LIMIT $2 OFFSET $3`,
expectedIndex: 'idx_interests_port',
maxRows: 5000,
},
{
name: 'Search clients (tsvector)',
sql: `SELECT * FROM clients WHERE port_id = $1 AND to_tsvector('simple', coalesce(full_name,'') || ' ' || coalesce(company_name,'')) @@ plainto_tsquery('simple', $2) LIMIT 10`,
expectedIndex: 'idx_clients_search_expr (GIN)',
maxRows: 10,
},
{
name: 'Search berths (trigram)',
sql: `SELECT * FROM berths WHERE port_id = $1 AND mooring_number % $2 ORDER BY similarity(mooring_number, $2) DESC LIMIT 10`,
expectedIndex: 'idx_berths_mooring_trgm (GIN)',
maxRows: 10,
},
{
name: 'Dashboard KPIs - total clients',
sql: `SELECT count(*) FROM clients WHERE port_id = $1 AND archived_at IS NULL`,
expectedIndex: 'idx_clients_port',
maxRows: 1,
},
{
name: 'Dashboard - pipeline counts',
sql: `SELECT pipeline_stage, count(*) FROM interests WHERE port_id = $1 AND archived_at IS NULL GROUP BY pipeline_stage`,
expectedIndex: 'idx_interests_port',
maxRows: 8,
},
{
name: 'Activity feed',
sql: `SELECT * FROM audit_logs WHERE port_id = $1 ORDER BY created_at DESC LIMIT 20`,
expectedIndex: 'idx_al_port',
maxRows: 20,
},
{
name: 'Notifications - unread count',
sql: `SELECT count(*) FROM notifications WHERE user_id = $1 AND port_id = $2 AND is_read = false`,
expectedIndex: 'idx_notif_user',
maxRows: 1,
},
{
name: 'Webhook dispatch - active webhooks for port',
sql: `SELECT * FROM webhooks WHERE port_id = $1 AND is_active = true AND events @> ARRAY[$2]`,
expectedIndex: 'idx_webhooks_port',
maxRows: 50,
},
{
name: 'Custom field values for entity',
sql: `SELECT cfv.*, cfd.* FROM custom_field_values cfv JOIN custom_field_definitions cfd ON cfv.field_id = cfd.id WHERE cfv.entity_id = $1 AND cfd.port_id = $2`,
expectedIndex: 'cfv_field_entity_idx, idx_cfd_port',
maxRows: 50,
},
];
describe('Query plan documentation', () => {
for (const query of CRITICAL_QUERIES) {
it(`${query.name} uses index ${query.expectedIndex}`, () => {
// Document the expected query plan.
// When running against a real DB, extend this test with:
// const result = await db.execute(`EXPLAIN ANALYZE ${query.sql}`, params);
// expect(result).toContain(query.expectedIndex);
expect(query.sql).toBeTruthy();
expect(query.expectedIndex).toBeTruthy();
expect(query.maxRows).toBeLessThanOrEqual(5000);
});
}
it('all 10 critical queries are documented', () => {
expect(CRITICAL_QUERIES.length).toBe(10);
});
it('every query targets a specific port scope via port_id', () => {
const portScopedQueries = CRITICAL_QUERIES.filter(
(q) => q.sql.includes('port_id'),
);
// All queries except the notifications unread-count (user_id primary) are port-scoped.
// Notifications also includes port_id, so all 10 should qualify.
expect(portScopedQueries.length).toBe(CRITICAL_QUERIES.length);
});
it('paginated queries cap maxRows at reasonable limits', () => {
const paginatedQueries = CRITICAL_QUERIES.filter((q) =>
q.sql.includes('LIMIT'),
);
paginatedQueries.forEach((q) => {
expect(q.maxRows).toBeLessThanOrEqual(5000);
});
});
it('full-text and trigram search queries use GIN indexes', () => {
const searchQueries = CRITICAL_QUERIES.filter((q) =>
q.expectedIndex.includes('GIN'),
);
expect(searchQueries.length).toBeGreaterThanOrEqual(2);
searchQueries.forEach((q) => {
expect(q.maxRows).toBeLessThanOrEqual(10);
});
});
it('all index names follow the project naming convention', () => {
// Indexes should be lowercase with underscores (or include GIN/note suffix).
const validPattern = /^[a-z0-9_,\s()]+$/i;
CRITICAL_QUERIES.forEach((q) => {
expect(q.expectedIndex).toMatch(validPattern);
});
});
});