Files
cfw-autumn/server/experiments/explainGetRewards.ts
2026-05-22 14:13:29 +01:00

152 lines
5.1 KiB
TypeScript

import { writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { AppEnv, schemas } from "@autumn/shared";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/node-postgres";
import pg from "pg";
import { initDrizzle, prodTestOrgId } from "./experimentEnv";
const { rewardRepo, rewardProgramRepo } = await import(
"../src/internal/rewards/repos/index"
);
// Run with:
// REWARDS_ORG_ID=org_2x3YWDWucn3OSul12pIV6XrcXyo bun run experiments/explainGetRewards.ts
const main = async () => {
const orgId = process.env.REWARDS_ORG_ID || prodTestOrgId;
const env = AppEnv.Live;
const { db } = initDrizzle();
console.log(`--- Running GET /products/rewards for org=${orgId} env=${env} ---\n`);
const startBoth = performance.now();
const [rewards, rewardPrograms] = await Promise.all([
rewardRepo.list({ db, orgId, env }),
rewardProgramRepo.list({ db, orgId, env }),
]);
const elapsedBoth = performance.now() - startBoth;
console.log(`Both queries (Promise.all): ${elapsedBoth.toFixed(2)}ms`);
console.log(` rewards: ${rewards.length} rows`);
console.log(` rewardPrograms: ${rewardPrograms.length} rows\n`);
const startRewards = performance.now();
const rewardsAlone = await rewardRepo.list({ db, orgId, env });
const elapsedRewards = performance.now() - startRewards;
console.log(`rewardRepo.list alone: ${elapsedRewards.toFixed(2)}ms (${rewardsAlone.length} rows)`);
const startPrograms = performance.now();
const programsAlone = await rewardProgramRepo.list({ db, orgId, env });
const elapsedPrograms = performance.now() - startPrograms;
console.log(`rewardProgramRepo.list alone: ${elapsedPrograms.toFixed(2)}ms (${programsAlone.length} rows)\n`);
// Capture the actual SQL Drizzle generates for rewardRepo.list by wiring up
// a separate drizzle client with a logger. This lets us EXPLAIN ANALYZE the
// real query — not a hand-rolled approximation.
const capturedQueries: { query: string; params: unknown[] }[] = [];
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 2,
});
const loggedDb = drizzle(pool, {
schema: schemas,
logger: {
logQuery: (query, params) => capturedQueries.push({ query, params }),
},
});
await rewardRepo.list({ db: loggedDb as never, orgId, env });
const rewardSql = capturedQueries.find((q) => q.query.includes("rewards"));
if (!rewardSql) {
console.log("Could not capture Drizzle SQL — bailing on EXPLAIN.\n");
} else {
console.log("--- Drizzle-generated SQL for rewardRepo.list ---\n");
console.log(rewardSql.query);
console.log("\nParams:", rewardSql.params, "\n");
console.log("--- EXPLAIN (ANALYZE, BUFFERS) on the actual Drizzle query ---\n");
const explainQuery = sql.raw(
`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${rewardSql.query}`,
);
// Bind the same params Drizzle bound.
const plan = await pool.query(
`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${rewardSql.query}`,
rewardSql.params as unknown[],
);
const planLines: string[] = [];
for (const row of plan.rows) {
const line = (row as Record<string, unknown>)["QUERY PLAN"];
if (typeof line === "string") planLines.push(line);
}
console.log(planLines.join("\n"));
const outDir = resolve(import.meta.dir, "out");
try {
const { mkdirSync } = await import("node:fs");
mkdirSync(outDir, { recursive: true });
} catch {
// ignore
}
const planPath = resolve(outDir, "rewards-explain.txt");
writeFileSync(planPath, planLines.join("\n"));
console.log(`\nWritten to: ${planPath}\n`);
const usedIndex = planLines.find((l) => /Index Scan|Index Only Scan/.test(l));
const seqScan = planLines.find((l) => /Seq Scan on entitlements/.test(l));
console.log("--- Verdict ---");
if (seqScan) {
console.log("❌ Still seq-scanning entitlements. Index not picked up.");
console.log(` ${seqScan.trim()}`);
} else if (usedIndex) {
console.log("✅ Index scan in plan:");
for (const l of planLines.filter((l) => /Index/.test(l))) {
console.log(` ${l.trim()}`);
}
}
console.log();
await pool.end();
}
console.log("--- Table sizes ---\n");
const sizes = await db.execute(sql`
SELECT
'rewards' AS table_name,
(SELECT count(*) FROM rewards) AS total_rows,
(SELECT count(*) FROM rewards WHERE org_id = ${orgId} AND env = ${env}) AS org_rows,
pg_size_pretty(pg_relation_size('rewards')) AS table_size
UNION ALL
SELECT
'reward_programs',
(SELECT count(*) FROM reward_programs),
(SELECT count(*) FROM reward_programs WHERE org_id = ${orgId} AND env = ${env}),
pg_size_pretty(pg_relation_size('reward_programs'))
UNION ALL
SELECT
'entitlements',
(SELECT count(*) FROM entitlements),
(SELECT count(*) FROM entitlements WHERE internal_reward_id IS NOT NULL),
pg_size_pretty(pg_relation_size('entitlements'))
`);
for (const row of sizes) {
console.log(row);
}
console.log();
console.log("--- Indexes on entitlements ---\n");
const indexes = await db.execute(sql`
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'entitlements'
ORDER BY indexname
`);
for (const row of indexes) {
console.log(row);
}
process.exit(0);
};
await main();