Files
gongxue-base/scripts/tenant-student-capacity.js
2026-07-12 19:26:57 +08:00

1115 lines
40 KiB
JavaScript

import { performance } from 'node:perf_hooks';
import fs from 'node:fs/promises';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import pg from 'pg';
import {
DESTRUCTIVE_TEST_CONFIRMATION,
assertDestructiveTestDatabase,
describeDatabaseTarget,
resolveDestructiveTestConfirmation,
} from './lib/destructive-test-database-guard.js';
const { Client } = pg;
export const CAPACITY_NAMESPACE = 'tiku.student-capacity.v1';
export const DEFAULT_TENANT_SLUG = 'capacity-test-students-100k';
export const MAX_STUDENT_COUNT = 100_000;
export const DEFAULT_STUDENT_COUNT = 100_000;
export const DEFAULT_BATCH_SIZE = 10_000;
export const DEFAULT_ITERATIONS = 10;
export const DEFAULT_WARMUP_ITERATIONS = 3;
export const DEFAULT_PAGE_LIMIT = 100;
export const DEFAULT_OUTPUT_DIR = 'docs/refactor/performance-reports';
const MODES = new Set(['plan', 'seed', 'benchmark', 'run', 'evidence', 'cleanup', 'smoke']);
const TENANT_SLUG_PATTERN = /^capacity-test-[a-z0-9](?:[a-z0-9-]{0,47}[a-z0-9])?$/;
const BLOCKED_TARGET_PATTERN = /(?:^|[-_.])tikupro(?:-pg)?(?:$|[-_.])/i;
function argumentValue(argv, name) {
const directIndex = argv.indexOf(name);
if (directIndex >= 0) return String(argv[directIndex + 1] || '').trim();
const prefix = `${name}=`;
const direct = argv.find(value => value.startsWith(prefix));
return direct ? String(direct).slice(prefix.length).trim() : '';
}
function integerOption(value, fallback, { name, min, max }) {
if (value === '') return fallback;
if (!/^\d+$/.test(value)) throw new Error(`${name} must be an integer`);
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {
throw new Error(`${name} must be between ${min} and ${max}`);
}
return parsed;
}
function validateTenantSlug(value) {
const slug = String(value || '').toLowerCase();
if (!TENANT_SLUG_PATTERN.test(slug)) {
throw new Error('tenant slug must match capacity-test-* and contain only lowercase letters, numbers, and hyphens');
}
return slug;
}
export function parseCapacityOptions(argv = process.argv.slice(2), env = process.env) {
const mode = argumentValue(argv, '--mode') || 'plan';
if (!MODES.has(mode)) throw new Error(`unsupported mode: ${mode}`);
const options = {
mode,
databaseUrl: String(env.DATABASE_URL || '').trim(),
tenantSlug: validateTenantSlug(
argumentValue(argv, '--tenant-slug') || env.CAPACITY_TENANT_SLUG || DEFAULT_TENANT_SLUG,
),
count: integerOption(argumentValue(argv, '--count'), DEFAULT_STUDENT_COUNT, {
name: 'count', min: 10, max: MAX_STUDENT_COUNT,
}),
batchSize: integerOption(argumentValue(argv, '--batch-size'), DEFAULT_BATCH_SIZE, {
name: 'batch-size', min: 100, max: 20_000,
}),
iterations: integerOption(argumentValue(argv, '--iterations'), DEFAULT_ITERATIONS, {
name: 'iterations', min: 1, max: 100,
}),
warmupIterations: integerOption(
argumentValue(argv, '--warmup-iterations'),
DEFAULT_WARMUP_ITERATIONS,
{ name: 'warmup-iterations', min: 0, max: 20 },
),
pageLimit: integerOption(argumentValue(argv, '--limit'), DEFAULT_PAGE_LIMIT, {
name: 'limit', min: 1, max: 500,
}),
outputDir: argumentValue(argv, '--output-dir') || env.CAPACITY_OUTPUT_DIR || DEFAULT_OUTPUT_DIR,
confirmation: resolveDestructiveTestConfirmation(env, argv),
};
if (mode !== 'plan' && !options.databaseUrl) {
throw new Error('DATABASE_URL is required outside plan mode');
}
return options;
}
function safeTarget(databaseUrl) {
if (!databaseUrl) return null;
const target = describeDatabaseTarget(databaseUrl);
const normalizedHost = target.host.replace(/^\[(.*)\]$/, '$1').toLowerCase();
if (
['127.0.0.1', 'localhost', '::1'].includes(normalizedHost) &&
target.port === '5432'
) {
throw new Error('Refusing tenant student capacity operation: local port 5432 is reserved for tikupro-pg');
}
for (const value of [target.host, target.database, target.user]) {
if (BLOCKED_TARGET_PATTERN.test(value)) {
throw new Error('Refusing tenant student capacity operation: tikupro-pg targets are forbidden');
}
}
return target;
}
function normalizePlanText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function percentile(values, target) {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil((target / 100) * sorted.length) - 1;
return sorted[Math.max(0, Math.min(sorted.length - 1, index))];
}
function round(value) {
return Math.round(Number(value || 0) * 1000) / 1000;
}
function containsSearchPattern(value) {
return `%${String(value).replace(/[\\%_]/g, match => `\\${match}`)}%`;
}
function studentLegacyId(tenantSlug, ordinal) {
return `${CAPACITY_NAMESPACE}:${tenantSlug}:${String(ordinal).padStart(6, '0')}`;
}
function capacityPlan(options) {
return {
schemaVersion: 1,
mode: options.mode,
dryRun: options.mode === 'plan',
target: safeTarget(options.databaseUrl),
fixture: {
namespace: CAPACITY_NAMESPACE,
tenantSlug: options.tenantSlug,
requestedStudents: options.count,
batchSize: options.batchSize,
tables: ['public.platform_users', 'public.tenant_memberships', 'public.student_profiles'],
},
benchmark: {
cases: ['first-page', 'deep-cursor', 'name-substring', 'phone-substring', 'email-substring'],
iterations: options.iterations,
warmupIterations: options.warmupIterations,
pageLimit: options.pageLimit,
explain: 'EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)',
},
applyRequirement: `--confirm=${DESTRUCTIVE_TEST_CONFIRMATION}`,
};
}
async function acquireHarnessLock(client, tenantSlug) {
const result = await client.query(
'select pg_try_advisory_lock(hashtext($1)) as acquired',
[`${CAPACITY_NAMESPACE}:${tenantSlug}`],
);
if (result.rows[0]?.acquired !== true) {
throw new Error(`capacity tenant ${tenantSlug} is already being modified or benchmarked`);
}
}
async function releaseHarnessLock(client, tenantSlug) {
await client.query('select pg_advisory_unlock(hashtext($1))', [`${CAPACITY_NAMESPACE}:${tenantSlug}`]);
}
async function loadManagedTenant(client, tenantSlug) {
const result = await client.query(
`
select id, slug::text as slug, name, owner_user_id as "ownerUserId", metadata
from public.tenants
where slug = $1::citext
limit 1
`,
[tenantSlug],
);
return result.rows[0] || null;
}
function assertManagedTenant(tenant, tenantSlug) {
const marker = tenant?.metadata?.capacityHarness;
if (
!tenant ||
marker?.namespace !== CAPACITY_NAMESPACE ||
marker?.managed !== true ||
marker?.tenantSlug !== tenantSlug ||
tenant.ownerUserId !== null
) {
throw new Error(`Refusing to use tenant ${tenantSlug}: capacity harness metadata marker does not match`);
}
}
async function ensureManagedTenant(client, tenantSlug) {
await client.query('begin');
try {
let tenant = await loadManagedTenant(client, tenantSlug);
if (tenant) {
assertManagedTenant(tenant, tenantSlug);
} else {
const inserted = await client.query(
`
insert into public.tenants (
slug, name, legal_name, status, mode, billing_status, metadata
)
values (
$1::citext,
'Capacity Test Students',
'Capacity Test Students - Non Production Only',
'active',
'saas',
'trial',
jsonb_build_object(
'capacityHarness',
jsonb_build_object(
'namespace', $2::text,
'managed', true,
'tenantSlug', $1::text
)
)
)
returning id, slug::text as slug, name, owner_user_id as "ownerUserId", metadata
`,
[tenantSlug, CAPACITY_NAMESPACE],
);
tenant = inserted.rows[0];
}
await client.query('commit');
return tenant;
} catch (error) {
await client.query('rollback').catch(() => undefined);
throw error;
}
}
async function managedFixtureCounts(client, tenantId, tenantSlug) {
const result = await client.query(
`
with managed_users as (
select id
from public.platform_users
where raw_profile #>> '{capacityHarness,namespace}' = $1
and raw_profile #>> '{capacityHarness,tenantSlug}' = $2
and raw_profile #>> '{capacityHarness,tenantId}' = $3::text
)
select
(select count(*)::integer from managed_users) as "platformUsers",
(
select count(*)::integer
from public.tenant_memberships tm
join managed_users u on u.id = tm.user_id
where tm.tenant_id = $3::uuid and tm.role = 'student'
) as "tenantMemberships",
(
select count(*)::integer
from public.student_profiles sp
join managed_users u on u.id = sp.user_id
where sp.tenant_id = $3::uuid
) as "studentProfiles"
`,
[CAPACITY_NAMESPACE, tenantSlug, tenantId],
);
return result.rows[0];
}
async function assertManagedUsersAreIsolated(client, tenantId, tenantSlug) {
const result = await client.query(
`
select
count(*) filter (
where u.auth_user_id is not null
)::integer as "linkedAuthUsers",
count(*) filter (
where coalesce(u.raw_profile #>> '{capacityHarness,tenantId}', '') <> $3::text
)::integer as "wrongTenantMarkers",
count(*) filter (
where exists (
select 1
from public.tenant_memberships tm
where tm.user_id = u.id and tm.tenant_id <> $3::uuid
)
)::integer as "crossTenantMemberships"
from public.platform_users u
where u.raw_profile #>> '{capacityHarness,namespace}' = $1
and u.raw_profile #>> '{capacityHarness,tenantSlug}' = $2
`,
[CAPACITY_NAMESPACE, tenantSlug, tenantId],
);
const isolation = result.rows[0];
if (
Number(isolation.linkedAuthUsers) > 0 ||
Number(isolation.wrongTenantMarkers) > 0 ||
Number(isolation.crossTenantMemberships) > 0
) {
throw new Error(`Refusing capacity fixture mutation: managed users are not isolated (${JSON.stringify(isolation)})`);
}
}
async function assertManagedTenantMemberships(client, tenantId, tenantSlug) {
const result = await client.query(
`
select count(*)::integer as "unmanagedMemberships"
from public.tenant_memberships tm
join public.platform_users u on u.id = tm.user_id
where tm.tenant_id = $3::uuid
and (
tm.role <> 'student'
or u.raw_profile #>> '{capacityHarness,namespace}' is distinct from $1
or u.raw_profile #>> '{capacityHarness,tenantSlug}' is distinct from $2
or u.raw_profile #>> '{capacityHarness,tenantId}' is distinct from $3::text
)
`,
[CAPACITY_NAMESPACE, tenantSlug, tenantId],
);
if (Number(result.rows[0]?.unmanagedMemberships || 0) > 0) {
throw new Error('Refusing capacity tenant operation: dedicated tenant contains unmanaged memberships');
}
}
async function assertBatchHasNoIdentityCollisions(client, tenantId, tenantSlug, start, end) {
const result = await client.query(
`
with source as (
select
ordinal,
$1::text || ':' || $2::text || ':' || lpad(ordinal::text, 6, '0') as legacy_id
from generate_series($4::integer, $5::integer) ordinal
)
select count(*)::integer as collisions
from source
join public.platform_users u on u.legacy_id = source.legacy_id
where u.raw_profile #>> '{capacityHarness,namespace}' is distinct from $1
or u.raw_profile #>> '{capacityHarness,tenantSlug}' is distinct from $2
or u.raw_profile #>> '{capacityHarness,tenantId}' is distinct from $3::text
`,
[CAPACITY_NAMESPACE, tenantSlug, tenantId, start, end],
);
if (Number(result.rows[0]?.collisions || 0) > 0) {
throw new Error(`Refusing capacity fixture seed: legacy identity collision in ordinals ${start}-${end}`);
}
}
async function seedBatch(client, tenantId, tenantSlug, start, end) {
await client.query('begin');
try {
await assertBatchHasNoIdentityCollisions(client, tenantId, tenantSlug, start, end);
const result = await client.query(
`
with source as materialized (
select
ordinal,
md5($1::text || ':' || $2::text || ':user:' || ordinal::text)::uuid as user_id,
$1::text || ':' || $2::text || ':' || lpad(ordinal::text, 6, '0') as legacy_id,
'capacity_' || replace($2::text, '-', '_') || '_' || lpad(ordinal::text, 6, '0') as username,
'student' || lpad(ordinal::text, 6, '0') || '@' || $2::text || '.capacity.invalid' as email,
'188' || lpad(ordinal::text, 8, '0') as phone,
'Capacity Student ' || lpad(ordinal::text, 6, '0') as name,
timestamptz '2024-01-01 00:00:00+00' + (ordinal * interval '1 second') as created_at
from generate_series($4::integer, $5::integer) ordinal
),
upsert_users as (
insert into public.platform_users (
id, legacy_id, username, email, phone, name, primary_role, status,
raw_profile, created_at, updated_at
)
select
source.user_id,
source.legacy_id,
source.username,
source.email::citext,
source.phone,
source.name,
'student',
'active',
jsonb_build_object(
'capacityHarness',
jsonb_build_object(
'namespace', $1::text,
'managed', true,
'tenantSlug', $2::text,
'tenantId', $3::text,
'ordinal', source.ordinal
)
),
source.created_at,
now()
from source
on conflict (legacy_id)
do update set
username = excluded.username,
email = excluded.email,
phone = excluded.phone,
name = excluded.name,
primary_role = 'student',
status = 'active',
raw_profile = excluded.raw_profile,
created_at = excluded.created_at,
updated_at = now()
where public.platform_users.raw_profile #>> '{capacityHarness,namespace}' = $1
and public.platform_users.raw_profile #>> '{capacityHarness,tenantSlug}' = $2
and public.platform_users.raw_profile #>> '{capacityHarness,tenantId}' = $3::text
returning id
),
upsert_memberships as (
insert into public.tenant_memberships (
tenant_id, user_id, role, status, permissions, created_at, updated_at
)
select $3::uuid, source.user_id, 'student', 'active', '{}'::jsonb, source.created_at, now()
from source
join upsert_users users on users.id = source.user_id
on conflict (tenant_id, user_id, role)
do update set
status = 'active',
permissions = '{}'::jsonb,
created_at = excluded.created_at,
updated_at = now()
returning id
),
upsert_profiles as (
insert into public.student_profiles (
tenant_id, user_id, questions_answered_today, mastered_words_count,
stats, progress, module_selections, recent_activities, created_at, updated_at
)
select
$3::uuid,
source.user_id,
source.ordinal % 80,
source.ordinal % 5000,
jsonb_build_object('capacityHarnessOrdinal', source.ordinal),
jsonb_build_object('completionPercent', source.ordinal % 101),
'{}'::jsonb,
'[]'::jsonb,
source.created_at,
now()
from source
join upsert_users users on users.id = source.user_id
on conflict (tenant_id, user_id)
do update set
questions_answered_today = excluded.questions_answered_today,
mastered_words_count = excluded.mastered_words_count,
stats = excluded.stats,
progress = excluded.progress,
module_selections = excluded.module_selections,
recent_activities = excluded.recent_activities,
created_at = excluded.created_at,
updated_at = now()
returning id
)
select
(select count(*)::integer from upsert_users) as users,
(select count(*)::integer from upsert_memberships) as memberships,
(select count(*)::integer from upsert_profiles) as profiles
`,
[CAPACITY_NAMESPACE, tenantSlug, tenantId, start, end],
);
const expected = end - start + 1;
const counts = result.rows[0];
for (const key of ['users', 'memberships', 'profiles']) {
if (Number(counts?.[key] || 0) !== expected) {
throw new Error(`capacity batch ${start}-${end} wrote ${counts?.[key] || 0} ${key}; expected ${expected}`);
}
}
await client.query('commit');
return counts;
} catch (error) {
await client.query('rollback').catch(() => undefined);
throw error;
}
}
async function seedFixture(client, options) {
const tenant = await ensureManagedTenant(client, options.tenantSlug);
assertManagedTenant(tenant, options.tenantSlug);
await assertManagedTenantMemberships(client, tenant.id, options.tenantSlug);
await assertManagedUsersAreIsolated(client, tenant.id, options.tenantSlug);
const before = await managedFixtureCounts(client, tenant.id, options.tenantSlug);
if (Number(before.platformUsers) > options.count) {
throw new Error(
`managed fixture already has ${before.platformUsers} users; cleanup is required before reducing it to ${options.count}`,
);
}
const batches = [];
for (let start = 1; start <= options.count; start += options.batchSize) {
const end = Math.min(options.count, start + options.batchSize - 1);
const counts = await seedBatch(client, tenant.id, options.tenantSlug, start, end);
batches.push({ start, end, ...counts });
console.error(`[capacity] seeded ${end}/${options.count} students`);
}
await client.query('analyze public.platform_users');
await client.query('analyze public.tenant_memberships');
await client.query('analyze public.student_profiles');
const after = await managedFixtureCounts(client, tenant.id, options.tenantSlug);
for (const key of ['platformUsers', 'tenantMemberships', 'studentProfiles']) {
if (Number(after[key]) !== options.count) {
throw new Error(`capacity fixture validation failed: ${key}=${after[key]}, expected ${options.count}`);
}
}
return { tenant, before, after, batches };
}
function buildStudentListQuery({ keyword = false, cursor = false, explain = false } = {}) {
const params = { tenantId: 1, status: 2 };
let nextParam = 3;
const filters = ['tm.tenant_id = $1', `tm.role = 'student'`, 'tm.status = $2'];
if (keyword) {
params.keyword = nextParam++;
filters.push(`(
coalesce(u.username, '') || ' ' ||
coalesce(u.name, '') || ' ' ||
coalesce(u.phone, '') || ' ' ||
coalesce(u.email::text, '')
) ilike $${params.keyword} escape '\\'`);
}
if (cursor) {
params.cursorCreatedAt = nextParam++;
params.cursorMembershipId = nextParam++;
filters.push(
`(tm.created_at, tm.id) < ($${params.cursorCreatedAt}::timestamptz, $${params.cursorMembershipId}::uuid)`,
);
}
params.limit = nextParam;
const sql = `
with student_page as materialized (
select tm.id, tm.tenant_id, tm.user_id, tm.status, tm.created_at, tm.updated_at
from public.tenant_memberships tm
join public.platform_users u on u.id = tm.user_id
left join public.student_profiles sp on sp.tenant_id = tm.tenant_id and sp.user_id = tm.user_id
where ${filters.join(' and ')}
order by tm.created_at desc, tm.id desc
limit $${params.limit}
),
class_agg as (
select tcm.tenant_id, tcm.user_id,
jsonb_agg(
jsonb_build_object(
'classId', tc.id,
'className', tc.name,
'classCode', tc.code,
'memberType', tcm.member_type,
'joinedAt', tcm.joined_at
)
order by tc.sort_order asc, tc.created_at desc
) filter (where tcm.status = 'active') as classes
from public.tenant_class_members tcm
join student_page page on page.tenant_id = tcm.tenant_id and page.user_id = tcm.user_id
join public.tenant_classes tc on tc.tenant_id = tcm.tenant_id and tc.id = tcm.class_id
where tcm.tenant_id = $1 and tcm.member_type = 'student'
group by tcm.tenant_id, tcm.user_id
)
select tm.id as "membershipId", tm.user_id as "userId", tm.status,
tm.created_at as "memberCreatedAt", tm.created_at::text as "cursorCreatedAt",
tm.updated_at as "memberUpdatedAt",
u.username, u.email::text as email, u.phone, u.name,
null::text as "avatarUrl", u.primary_role as "primaryRole",
u.last_seen_at as "lastSeenAt",
sp.id as "profileId", sp.region_id as "regionId", r.name as "regionName",
sp.selected_school_id as "selectedSchoolId", s.name as "selectedSchoolName",
sp.selected_major_id as "selectedMajorId", m.name as "selectedMajorName",
sp.questions_answered_today as "questionsAnsweredToday",
sp.mastered_words_count as "masteredWordsCount",
sp.last_check_in_date as "lastCheckInDate",
sp.stats, sp.progress, sp.module_selections as "moduleSelections",
coalesce(ca.classes, '[]'::jsonb) as classes
from student_page tm
join public.platform_users u on u.id = tm.user_id
left join public.student_profiles sp on sp.tenant_id = tm.tenant_id and sp.user_id = tm.user_id
left join public.regions r on r.tenant_id = tm.tenant_id and r.id = sp.region_id
left join public.schools s on s.tenant_id = tm.tenant_id and s.id = sp.selected_school_id
left join public.majors m on m.tenant_id = tm.tenant_id and m.id = sp.selected_major_id
left join class_agg ca on ca.tenant_id = tm.tenant_id and ca.user_id = tm.user_id
order by tm.created_at desc, tm.id desc
`;
return {
sql: explain ? `explain (analyze, buffers, format json) ${sql}` : sql,
params,
};
}
async function assertBenchmarkIndexes(client) {
const names = [
'idx_memberships_student_keyset_page',
'idx_platform_users_identity_search_trgm',
];
const result = await client.query(
`
select
index_name,
to_regclass('public.' || index_name)::text as relation,
pg_get_indexdef(to_regclass('public.' || index_name)) as definition
from unnest($1::text[]) index_name
order by index_name
`,
[names],
);
const missing = result.rows.filter(row => !row.relation || !row.definition);
if (missing.length) {
throw new Error(`required student capacity indexes are missing: ${missing.map(row => row.index_name).join(', ')}`);
}
return result.rows.map(row => ({ name: row.index_name, definition: normalizePlanText(row.definition) }));
}
async function loadBenchmarkFixture(client, tenantSlug) {
const tenant = await loadManagedTenant(client, tenantSlug);
assertManagedTenant(tenant, tenantSlug);
await assertManagedTenantMemberships(client, tenant.id, tenantSlug);
await assertManagedUsersAreIsolated(client, tenant.id, tenantSlug);
const counts = await managedFixtureCounts(client, tenant.id, tenantSlug);
if (
Number(counts.platformUsers) < 10 ||
counts.platformUsers !== counts.tenantMemberships ||
counts.platformUsers !== counts.studentProfiles
) {
throw new Error(`capacity fixture is incomplete: ${JSON.stringify(counts)}`);
}
return { tenant, counts, count: Number(counts.platformUsers) };
}
async function loadBenchmarkTargets(client, tenantId, tenantSlug, count) {
const searchOrdinal = Math.max(1, Math.floor(count / 2));
const deepOrdinal = Math.max(2, Math.floor(count * 0.1));
const result = await client.query(
`
select
u.legacy_id as "legacyId",
u.name,
u.phone,
u.email::text as email,
tm.id as "membershipId",
tm.created_at as "createdAt"
from public.platform_users u
join public.tenant_memberships tm
on tm.tenant_id = $1 and tm.user_id = u.id and tm.role = 'student'
where u.legacy_id = any($2::text[])
`,
[tenantId, [studentLegacyId(tenantSlug, searchOrdinal), studentLegacyId(tenantSlug, deepOrdinal)]],
);
const byLegacyId = new Map(result.rows.map(row => [row.legacyId, row]));
const search = byLegacyId.get(studentLegacyId(tenantSlug, searchOrdinal));
const deep = byLegacyId.get(studentLegacyId(tenantSlug, deepOrdinal));
if (!search || !deep) throw new Error('capacity benchmark target rows are missing');
return {
searchOrdinal,
deepOrdinal,
approximateDeepCursorOffset: count - deepOrdinal,
search,
deep,
};
}
function explainSummary(document) {
const root = Array.isArray(document) ? document[0] : document;
const plan = root?.Plan || {};
const nodeTypes = new Set();
const indexes = new Set();
function visit(node) {
if (!node || typeof node !== 'object') return;
if (node['Node Type']) nodeTypes.add(node['Node Type']);
if (node['Index Name']) indexes.add(node['Index Name']);
for (const child of node.Plans || []) visit(child);
}
visit(plan);
return {
planningTimeMs: round(root?.['Planning Time']),
executionTimeMs: round(root?.['Execution Time']),
actualRows: Number(plan['Actual Rows'] || 0),
nodeTypes: [...nodeTypes],
indexes: [...indexes],
buffers: {
sharedHit: Number(plan['Shared Hit Blocks'] || 0),
sharedRead: Number(plan['Shared Read Blocks'] || 0),
sharedDirtied: Number(plan['Shared Dirtied Blocks'] || 0),
sharedWritten: Number(plan['Shared Written Blocks'] || 0),
localHit: Number(plan['Local Hit Blocks'] || 0),
localRead: Number(plan['Local Read Blocks'] || 0),
tempRead: Number(plan['Temp Read Blocks'] || 0),
tempWritten: Number(plan['Temp Written Blocks'] || 0),
},
};
}
async function benchmarkCase(client, definition, options) {
const query = buildStudentListQuery(definition.shape);
for (let index = 0; index < options.warmupIterations; index += 1) {
await client.query(query.sql, definition.values);
}
const latencies = [];
const rowCounts = [];
for (let index = 0; index < options.iterations; index += 1) {
const startedAt = performance.now();
const result = await client.query(query.sql, definition.values);
latencies.push(performance.now() - startedAt);
rowCounts.push(result.rowCount);
}
const explainQuery = buildStudentListQuery({ ...definition.shape, explain: true });
const explainResult = await client.query(explainQuery.sql, definition.values);
const explain = explainResult.rows[0]?.['QUERY PLAN'];
return {
id: definition.id,
label: definition.label,
searchField: definition.searchField || null,
keyword: definition.keyword || null,
cursor: definition.cursor || null,
iterations: options.iterations,
rows: {
min: Math.min(...rowCounts),
max: Math.max(...rowCounts),
},
latencyMs: {
min: round(Math.min(...latencies)),
p50: round(percentile(latencies, 50)),
p95: round(percentile(latencies, 95)),
max: round(Math.max(...latencies)),
},
explain: {
summary: explainSummary(explain),
document: explain,
},
};
}
async function benchmarkFixture(client, options, safety) {
const fixture = await loadBenchmarkFixture(client, options.tenantSlug);
const indexes = await assertBenchmarkIndexes(client);
const targets = await loadBenchmarkTargets(
client,
fixture.tenant.id,
options.tenantSlug,
fixture.count,
);
const limitValue = options.pageLimit + 1;
const nameKeyword = String(targets.search.name || '').replace(/^Capacity /, '');
const phoneKeyword = String(targets.search.phone || '').slice(-7);
const emailKeyword = String(targets.search.email || '').match(/\d{6}@/)?.[0] || String(targets.search.email || '');
const definitions = [
{
id: 'first-page',
label: 'Tenant student first page',
shape: {},
values: [fixture.tenant.id, 'active', limitValue],
},
{
id: 'deep-cursor',
label: 'Tenant student deep cursor page',
shape: { cursor: true },
values: [fixture.tenant.id, 'active', targets.deep.createdAt, targets.deep.membershipId, limitValue],
cursor: {
approximateOffset: targets.approximateDeepCursorOffset,
anchorOrdinal: targets.deepOrdinal,
},
},
{
id: 'name-substring',
label: 'Student name substring search',
searchField: 'name',
keyword: nameKeyword,
shape: { keyword: true },
values: [fixture.tenant.id, 'active', containsSearchPattern(nameKeyword), limitValue],
},
{
id: 'phone-substring',
label: 'Student phone substring search',
searchField: 'phone',
keyword: phoneKeyword,
shape: { keyword: true },
values: [fixture.tenant.id, 'active', containsSearchPattern(phoneKeyword), limitValue],
},
{
id: 'email-substring',
label: 'Student email substring search',
searchField: 'email',
keyword: emailKeyword,
shape: { keyword: true },
values: [fixture.tenant.id, 'active', containsSearchPattern(emailKeyword), limitValue],
},
];
const cases = [];
for (const definition of definitions) {
console.error(`[capacity] benchmarking ${definition.id}`);
cases.push(await benchmarkCase(client, definition, options));
}
return {
schemaVersion: 1,
kind: 'tenant-student-capacity',
generatedAt: new Date().toISOString(),
safety: {
databaseEnvironment: safety.environment,
databaseTarget: safety.target,
namespace: CAPACITY_NAMESPACE,
tenantSlug: options.tenantSlug,
productionAllowed: false,
},
fixture: {
platformUsers: Number(fixture.counts.platformUsers),
tenantMemberships: Number(fixture.counts.tenantMemberships),
studentProfiles: Number(fixture.counts.studentProfiles),
},
config: {
iterations: options.iterations,
warmupIterations: options.warmupIterations,
pageLimit: options.pageLimit,
rawQueryLimit: limitValue,
deepCursorApproximateOffset: targets.approximateDeepCursorOffset,
},
indexes,
cases,
limitations: [
'Synthetic non-production data only.',
'Latency samples are warm-cache observations from the current test host, not a production SLA.',
'The harness validates the unscoped tenant-admin student list shape; class/region filters require separate workload evidence.',
],
};
}
function reportMarkdown(report) {
const lines = [
'# Tenant Student Capacity Evidence',
'',
`Generated: ${report.generatedAt}`,
'',
`Database environment: \`${report.safety.databaseEnvironment}\``,
'',
`Dedicated tenant: \`${report.safety.tenantSlug}\``,
'',
`Managed rows: ${report.fixture.platformUsers} platform users / ${report.fixture.tenantMemberships} memberships / ${report.fixture.studentProfiles} profiles`,
'',
...(report.timingsMs
? [
`Harness timings: ${Object.entries(report.timingsMs).map(([key, value]) => `${key}=${value}ms`).join(', ')}`,
'',
]
: []),
'| Case | Rows | P50 ms | P95 ms | EXPLAIN ms | Shared hit/read | Plan indexes |',
'| --- | ---: | ---: | ---: | ---: | ---: | --- |',
];
for (const item of report.cases) {
const summary = item.explain.summary;
lines.push(
`| ${item.id} | ${item.rows.min}-${item.rows.max} | ${item.latencyMs.p50} | ${item.latencyMs.p95} | ${summary.executionTimeMs} | ${summary.buffers.sharedHit}/${summary.buffers.sharedRead} | ${summary.indexes.join(', ') || 'none'} |`,
);
}
lines.push('', '## Index Definitions', '');
for (const index of report.indexes) {
lines.push(`### ${index.name}`, '', '```sql', index.definition, '```', '');
}
lines.push(
'## Interpretation Limits',
'',
...report.limitations.map(item => `- ${item}`),
'',
'The full JSON report contains PostgreSQL `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` documents for every case.',
'',
);
return `${lines.join('\n')}\n`;
}
function fileTimestamp(date = new Date()) {
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
}
async function writeReport(report, outputDir) {
const absoluteOutputDir = path.resolve(outputDir);
await fs.mkdir(absoluteOutputDir, { recursive: true });
const stamp = fileTimestamp(new Date(report.generatedAt));
const jsonPath = path.join(absoluteOutputDir, `tenant-student-capacity-${stamp}.json`);
const markdownPath = path.join(absoluteOutputDir, `tenant-student-capacity-${stamp}.md`);
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
await fs.writeFile(markdownPath, reportMarkdown(report), 'utf8');
return { jsonPath, markdownPath };
}
async function cleanupManagedFixture(client, tenantSlug) {
const tenant = await loadManagedTenant(client, tenantSlug);
let tenantId = null;
if (tenant) {
assertManagedTenant(tenant, tenantSlug);
tenantId = tenant.id;
await assertManagedTenantMemberships(client, tenant.id, tenantSlug);
await assertManagedUsersAreIsolated(client, tenant.id, tenantSlug);
} else {
const orphanSafety = await client.query(
`
select count(*)::integer as unsafe
from public.platform_users u
where u.raw_profile #>> '{capacityHarness,namespace}' = $1
and u.raw_profile #>> '{capacityHarness,tenantSlug}' = $2
and (
u.auth_user_id is not null
or exists (select 1 from public.tenant_memberships tm where tm.user_id = u.id)
)
`,
[CAPACITY_NAMESPACE, tenantSlug],
);
if (Number(orphanSafety.rows[0]?.unsafe || 0) > 0) {
throw new Error('Refusing orphan capacity cleanup: managed users still have auth or tenant membership references');
}
}
let deletedTenant = 0;
if (tenantId) {
const deleted = await client.query(
`
delete from public.tenants
where id = $1
and metadata #>> '{capacityHarness,namespace}' = $2
and metadata #>> '{capacityHarness,tenantSlug}' = $3
`,
[tenantId, CAPACITY_NAMESPACE, tenantSlug],
);
deletedTenant = deleted.rowCount;
}
let deletedUsers = 0;
while (true) {
const deleted = await client.query(
`
with doomed as (
select id
from public.platform_users
where raw_profile #>> '{capacityHarness,namespace}' = $1
and raw_profile #>> '{capacityHarness,tenantSlug}' = $2
and auth_user_id is null
and not exists (
select 1 from public.tenant_memberships tm where tm.user_id = public.platform_users.id
)
order by id
limit 5000
)
delete from public.platform_users users
using doomed
where users.id = doomed.id
`,
[CAPACITY_NAMESPACE, tenantSlug],
);
deletedUsers += deleted.rowCount;
if (deleted.rowCount === 0) break;
}
const remaining = await client.query(
`
with managed_users as (
select id
from public.platform_users
where raw_profile #>> '{capacityHarness,namespace}' = $1
and raw_profile #>> '{capacityHarness,tenantSlug}' = $2
)
select
(
select count(*)::integer
from public.tenants
where slug = $2::citext
and metadata #>> '{capacityHarness,namespace}' = $1
and metadata #>> '{capacityHarness,tenantSlug}' = $2
) as tenants,
(select count(*)::integer from managed_users) as "platformUsers",
(
select count(*)::integer
from public.tenant_memberships membership
join managed_users users on users.id = membership.user_id
) as memberships,
(
select count(*)::integer
from public.student_profiles profile
join managed_users users on users.id = profile.user_id
) as profiles
`,
[CAPACITY_NAMESPACE, tenantSlug],
);
const remainingCounts = remaining.rows[0] || {};
const cleanupVerified = Object.values(remainingCounts).every(value => Number(value || 0) === 0);
if (!cleanupVerified) {
throw new Error(`capacity cleanup left managed rows behind: ${JSON.stringify(remainingCounts)}`);
}
return { deletedTenant, deletedUsers, cleanupVerified, remaining: remainingCounts };
}
async function runDatabaseMode(options) {
const totalStartedAt = performance.now();
safeTarget(options.databaseUrl);
const client = new Client({
connectionString: options.databaseUrl,
application_name: 'tiku-tenant-student-capacity',
});
await client.connect();
let locked = false;
try {
const safety = await assertDestructiveTestDatabase({
client,
databaseUrl: options.databaseUrl,
confirmation: options.confirmation,
operation: `tenant student capacity ${options.mode}`,
});
await client.query(`select set_config('lock_timeout', '5s', false)`);
await client.query(`select set_config('statement_timeout', '300s', false)`);
await acquireHarnessLock(client, options.tenantSlug);
locked = true;
if (options.mode === 'cleanup') {
const cleanupStartedAt = performance.now();
const cleanup = await cleanupManagedFixture(client, options.tenantSlug);
return {
mode: options.mode,
cleanup,
safety,
timingsMs: {
cleanup: round(performance.now() - cleanupStartedAt),
total: round(performance.now() - totalStartedAt),
},
};
}
let seed = null;
let report = null;
let reportPaths = null;
let primaryError = null;
const timingsMs = {};
try {
if (['seed', 'run', 'evidence', 'smoke'].includes(options.mode)) {
const seedStartedAt = performance.now();
seed = await seedFixture(client, options);
timingsMs.seed = round(performance.now() - seedStartedAt);
}
if (['benchmark', 'run', 'evidence', 'smoke'].includes(options.mode)) {
const benchmarkStartedAt = performance.now();
report = await benchmarkFixture(client, options, safety);
timingsMs.benchmark = round(performance.now() - benchmarkStartedAt);
report.timingsMs = { ...timingsMs };
if (options.mode !== 'evidence') reportPaths = await writeReport(report, options.outputDir);
}
} catch (error) {
primaryError = error;
}
let cleanup = null;
if (['smoke', 'evidence'].includes(options.mode)) {
try {
const cleanupStartedAt = performance.now();
cleanup = await cleanupManagedFixture(client, options.tenantSlug);
timingsMs.cleanup = round(performance.now() - cleanupStartedAt);
} catch (cleanupError) {
if (primaryError) {
primaryError.message += `; cleanup also failed: ${cleanupError.message}`;
} else {
primaryError = cleanupError;
}
}
}
if (primaryError) throw primaryError;
if (report && options.mode === 'evidence') {
report.cleanup = cleanup;
report.timingsMs = {
...timingsMs,
total: round(performance.now() - totalStartedAt),
};
reportPaths = await writeReport(report, options.outputDir);
}
return {
mode: options.mode,
safety,
seed: seed ? { tenantId: seed.tenant.id, counts: seed.after, batches: seed.batches.length } : null,
report: report ? { fixture: report.fixture, paths: reportPaths } : null,
cleanup,
timingsMs: {
...timingsMs,
total: round(performance.now() - totalStartedAt),
},
};
} finally {
if (locked) await releaseHarnessLock(client, options.tenantSlug).catch(() => undefined);
await client.end().catch(() => undefined);
}
}
export async function main(argv = process.argv.slice(2), env = process.env) {
const options = parseCapacityOptions(argv, env);
if (options.mode === 'plan') {
console.log(JSON.stringify(capacityPlan(options), null, 2));
return;
}
const result = await runDatabaseMode(options);
console.log(JSON.stringify(result, null, 2));
}
const invokedPath = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : '';
if (import.meta.url === invokedPath) {
main().catch(error => {
console.error(`[capacity] ${error.message}`);
process.exitCode = 1;
});
}
export {
buildStudentListQuery,
capacityPlan,
containsSearchPattern,
reportMarkdown,
safeTarget,
studentLegacyId,
};