Files
cfw-autumn/server/experiments/diffOptimizedVsCurrent.ts
2026-05-14 10:19:12 +01:00

181 lines
5.9 KiB
TypeScript

import { AppEnv } from "@autumn/shared";
import type { FlattenedCustomerRow } from "../src/internal/customers/reassembleFlattenedCustomer/index.js";
import chalk from "chalk";
import { sql } from "drizzle-orm";
import { RELEVANT_STATUSES } from "../src/internal/customers/cusProducts/CusProductService";
import { initDrizzle } from "../src/db/initDrizzle";
import { loadLocalEnv } from "../src/utils/envUtils";
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
import { reassembleFlattenedCustomer } from "../src/internal/customers/reassembleFlattenedCustomer";
import { getOptimizedFullCusQuery } from "./optimizedFullCusQuery";
loadLocalEnv();
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
type Args = {
orgId: string;
env: AppEnv;
inStatuses: typeof RELEVANT_STATUSES;
withSubs: boolean;
limit: number;
cusProductLimit: number;
};
const sharedArgs: Args & {
cursor?: { v: 0; t: number; id: string };
} = {
orgId: ORG_ID,
env: AppEnv.Sandbox,
inStatuses: RELEVANT_STATUSES,
withSubs: true,
limit: parseInt(process.env.LIMIT ?? "50", 10),
cusProductLimit: 15,
};
type AnyRecord = Record<string, unknown>;
const fetchFlat = async (
db: ReturnType<typeof initDrizzle>["db"],
q: any,
): Promise<FlattenedCustomerRow> => {
const rows = await db.execute(q);
const flat = (rows as unknown as AnyRecord[])[0] as unknown as FlattenedCustomerRow;
return flat;
};
const sortById = (a: { id?: string }, b: { id?: string }) =>
(a.id ?? "").localeCompare(b.id ?? "");
const sortDeep = (obj: unknown): unknown => {
if (Array.isArray(obj)) {
const sorted = obj.map(sortDeep);
if (sorted.length > 0 && typeof sorted[0] === "object" && sorted[0] !== null) {
(sorted as AnyRecord[]).sort((a, b) => {
const ka = (a as AnyRecord).id ?? (a as AnyRecord).internal_id ?? "";
const kb = (b as AnyRecord).id ?? (b as AnyRecord).internal_id ?? "";
return String(ka).localeCompare(String(kb));
});
}
return sorted;
}
if (obj && typeof obj === "object") {
const entries = Object.entries(obj as AnyRecord)
.map(([k, v]) => [k, sortDeep(v)] as const)
.sort(([a], [b]) => a.localeCompare(b));
return Object.fromEntries(entries);
}
return obj;
};
const main = async () => {
const { db, client } = initDrizzle();
try {
const offsetPct = parseInt(process.env.OFFSET_PCT ?? "0", 10);
if (offsetPct > 0) {
const total = (await db.execute(
sql`SELECT COUNT(*)::int AS n FROM customers WHERE org_id = ${ORG_ID} AND env = 'sandbox'`,
)) as unknown as { n: number }[];
const offset = Math.floor((total[0].n * offsetPct) / 100);
const row = (await db.execute(
sql`SELECT created_at, id FROM customers WHERE org_id = ${ORG_ID} AND env = 'sandbox' ORDER BY created_at DESC, id DESC LIMIT 1 OFFSET ${offset}`,
)) as unknown as { created_at: number; id: string }[];
sharedArgs.cursor = { v: 0, t: row[0].created_at, id: row[0].id };
console.log(chalk.cyan(`Using deep cursor at ${offsetPct}%: ${row[0].id}`));
}
console.log(chalk.cyan(`Running CURRENT query (limit=${sharedArgs.limit}, cursor=${sharedArgs.cursor ? "yes" : "no"})...`));
const flatCurrent = await fetchFlat(
db,
getCursorPaginatedFullCusQuery(sharedArgs),
);
console.log(chalk.cyan(`Running OPTIMIZED query...`));
const flatOptim = await fetchFlat(
db,
getOptimizedFullCusQuery(sharedArgs),
);
// Compare top-level array lengths
const keys = [
"customers",
"customer_products",
"customer_entitlements",
"extra_customer_entitlements",
"customer_prices",
"entitlements",
"rollovers",
"replaceables",
"free_trials",
"subscriptions",
] as const;
console.log(chalk.bold("\nTop-level counts:"));
let lenMismatch = false;
for (const k of keys) {
const a = (flatCurrent[k] as unknown[] | undefined)?.length ?? 0;
const b = (flatOptim[k] as unknown[] | undefined)?.length ?? 0;
const match = a === b;
if (!match) lenMismatch = true;
console.log(
` ${k.padEnd(32)} current=${String(a).padStart(5)} optim=${String(b).padStart(5)} ${match ? chalk.green("✓") : chalk.red("✗")}`,
);
}
if (lenMismatch) {
console.log(chalk.red("\n❌ Length mismatch — aborting deep compare"));
process.exit(1);
}
// Reassemble both and deep compare the FullCustomer arrays
const customersA = reassembleFlattenedCustomer(flatCurrent);
const customersB = reassembleFlattenedCustomer(flatOptim);
console.log(chalk.cyan(`\nReassembled ${customersA.length} customers (both queries)`));
if (customersA.length !== customersB.length) {
console.log(chalk.red(`Mismatch in reassembled customer counts: ${customersA.length} vs ${customersB.length}`));
process.exit(1);
}
// Sort by id for stable diff (same cursor order should already be aligned)
customersA.sort((a: any, b: any) => (a.internal_id ?? "").localeCompare(b.internal_id ?? ""));
customersB.sort((a: any, b: any) => (a.internal_id ?? "").localeCompare(b.internal_id ?? ""));
let diffs = 0;
for (let i = 0; i < customersA.length; i++) {
const a = sortDeep(customersA[i]);
const b = sortDeep(customersB[i]);
const ja = JSON.stringify(a);
const jb = JSON.stringify(b);
if (ja !== jb) {
diffs++;
if (diffs <= 3) {
console.log(chalk.red(`\nDIFF for customer #${i} (${(customersA[i] as any).id}):`));
// find first differing key
for (const k of Object.keys(a as AnyRecord)) {
const va = JSON.stringify((a as AnyRecord)[k]);
const vb = JSON.stringify((b as AnyRecord)[k]);
if (va !== vb) {
console.log(` KEY ${k}:`);
console.log(chalk.gray(` current: ${va?.slice(0, 200)}`));
console.log(chalk.gray(` optim: ${vb?.slice(0, 200)}`));
}
}
}
}
}
if (diffs === 0) {
console.log(chalk.green(`\n✅ All ${customersA.length} customers match byte-for-byte`));
} else {
console.log(chalk.red(`\n❌ ${diffs}/${customersA.length} customers differ`));
}
} finally {
await client.end();
process.exit(0);
}
};
await main();