feat: 🎸 cleanup gitdiff
This commit is contained in:
@@ -1,153 +0,0 @@
|
||||
import { AppEnv, RELEVANT_STATUSES } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { CusBatchService } from "../src/internal/customers/CusBatchService";
|
||||
import { CusSearchService } from "../src/internal/customers/CusSearchService";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import {
|
||||
type FlattenedCustomerRow,
|
||||
reassembleFlattenedCustomer,
|
||||
} from "../src/internal/customers/reassembleFlattenedCustomer";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
const ENV = AppEnv.Sandbox;
|
||||
const REPEATS = 5;
|
||||
const LIMIT = parseInt(process.env.LIMIT ?? "500", 10);
|
||||
const OFFSET_PCT = parseInt(process.env.OFFSET_PCT ?? "95", 10);
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Dashboard Merge Bench ================\n",
|
||||
),
|
||||
);
|
||||
const { db, client } = initDrizzle();
|
||||
const ctx = {
|
||||
db,
|
||||
client,
|
||||
org: { id: ORG_ID, slug: "unit-test-org" },
|
||||
env: ENV,
|
||||
logger: {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
warn: () => {},
|
||||
debug: () => {},
|
||||
trace: () => {},
|
||||
},
|
||||
} as any;
|
||||
|
||||
try {
|
||||
const total = (
|
||||
(await db.execute(
|
||||
sql`SELECT COUNT(*)::int AS n FROM customers WHERE org_id = ${ORG_ID} AND env = ${ENV}`,
|
||||
)) as unknown as { n: number }[]
|
||||
)[0].n;
|
||||
const offset = Math.floor((total * OFFSET_PCT) / 100);
|
||||
const row = (await db.execute(
|
||||
sql`SELECT created_at, id FROM customers WHERE org_id = ${ORG_ID} AND env = ${ENV} ORDER BY created_at DESC, id DESC LIMIT 1 OFFSET ${offset}`,
|
||||
)) as unknown as { created_at: number; id: string }[];
|
||||
const cursor = { t: Number(row[0].created_at), id: row[0].id };
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` total=${total.toLocaleString()} cursor at ${OFFSET_PCT}%: ${cursor.id} limit=${LIMIT}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const measure = async (label: string, fn: () => Promise<void>) => {
|
||||
for (let i = 0; i < 2; i++) await fn();
|
||||
const samples: number[] = [];
|
||||
for (let i = 0; i < REPEATS; i++) {
|
||||
const t0 = performance.now();
|
||||
await fn();
|
||||
samples.push(performance.now() - t0);
|
||||
}
|
||||
samples.sort((a, b) => a - b);
|
||||
const p50 = samples[Math.floor(samples.length / 2)];
|
||||
const min = samples[0];
|
||||
const max = samples[samples.length - 1];
|
||||
console.log(
|
||||
` ${chalk.cyan(label.padEnd(20))} p50=${p50.toFixed(0).padStart(4)}ms min=${min.toFixed(0).padStart(4)}ms max=${max.toFixed(0).padStart(4)}ms`,
|
||||
);
|
||||
return p50;
|
||||
};
|
||||
|
||||
const oldPath = async () => {
|
||||
const { internalIds } =
|
||||
await CusSearchService.resolveInternalIdsByCursor({
|
||||
db,
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
search: "",
|
||||
cursor,
|
||||
limit: LIMIT,
|
||||
});
|
||||
if (internalIds.length === 0) return;
|
||||
const query = getCursorPaginatedFullCusQuery({
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
withSubs: true,
|
||||
limit: internalIds.length,
|
||||
internalCustomerIds: internalIds,
|
||||
cusProductLimit: 15,
|
||||
});
|
||||
const rows = (await db.execute(query)) as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
const flat = (rows[0] ?? {}) as unknown as FlattenedCustomerRow;
|
||||
reassembleFlattenedCustomer(flat);
|
||||
};
|
||||
|
||||
const newPath = async () => {
|
||||
await CusBatchService.getDashboardCursorPage({
|
||||
ctx,
|
||||
search: "",
|
||||
filters: undefined,
|
||||
cursor,
|
||||
limit: LIMIT,
|
||||
});
|
||||
};
|
||||
|
||||
const oldWithStatus = async () => {
|
||||
// Call old path manually (resolve + fetch) with status filter for parity
|
||||
await oldPath();
|
||||
};
|
||||
|
||||
const newWithStatus = async () => {
|
||||
await CusBatchService.getDashboardCursorPage({
|
||||
ctx,
|
||||
search: "",
|
||||
filters: { status: ["active"] },
|
||||
cursor,
|
||||
limit: LIMIT,
|
||||
});
|
||||
};
|
||||
|
||||
const oldP50 = await measure("OLD (resolve+fetch)", oldPath);
|
||||
const newP50 = await measure("NEW (no filters)", newPath);
|
||||
const newStatusP50 = await measure(
|
||||
"NEW (status=active)",
|
||||
newWithStatus,
|
||||
);
|
||||
|
||||
console.log();
|
||||
console.log(chalk.magentaBright("================ Summary ================"));
|
||||
const delta = (((newP50 - oldP50) / oldP50) * 100).toFixed(0);
|
||||
console.log(
|
||||
` old → new (no filters): ${oldP50.toFixed(0)}ms → ${newP50.toFixed(0)}ms (${delta}%)`,
|
||||
);
|
||||
console.log(
|
||||
` new with status=active: ${newStatusP50.toFixed(0)}ms (uses resolve+fetch fallback)`,
|
||||
);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,24 +0,0 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
const rows = (await db.execute(sql`
|
||||
SELECT indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename = 'customer_products'
|
||||
AND (indexname LIKE '%revenuecat%' OR indexdef LIKE '%revenuecat%')
|
||||
`)) as any[];
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,338 +0,0 @@
|
||||
import type { AppEnv, CusProductStatus, ListCustomersV2Params } from "@autumn/shared";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { getCustomerListFilterSql } from "../src/internal/customers/getFullCusQuery";
|
||||
|
||||
export type CursorPaginatedFullCusQueryArgs = {
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
inStatuses?: CusProductStatus[];
|
||||
includeInvoices: boolean;
|
||||
withEntities: boolean;
|
||||
withTrialsUsed: boolean;
|
||||
withSubs: boolean;
|
||||
limit: number;
|
||||
cursor?: { createdAt: number; id: string };
|
||||
withEvents?: boolean;
|
||||
internalCustomerIds?: string[];
|
||||
plans?: ListCustomersV2Params["plans"];
|
||||
processors?: ListCustomersV2Params["processors"];
|
||||
search?: string;
|
||||
cusProductLimit: number;
|
||||
};
|
||||
|
||||
export const getCursorPaginatedFullCusQuery = ({
|
||||
orgId,
|
||||
env,
|
||||
inStatuses,
|
||||
includeInvoices,
|
||||
withEntities,
|
||||
withTrialsUsed,
|
||||
withSubs,
|
||||
limit,
|
||||
cursor,
|
||||
withEvents = false,
|
||||
internalCustomerIds,
|
||||
plans,
|
||||
processors,
|
||||
search,
|
||||
cusProductLimit,
|
||||
}: CursorPaginatedFullCusQueryArgs) => {
|
||||
const withStatusFilter = () => {
|
||||
return inStatuses?.length
|
||||
? sql`AND cp.status = ANY(ARRAY[${sql.join(
|
||||
inStatuses.map((status) => sql`${status}`),
|
||||
sql`, `,
|
||||
)}])`
|
||||
: sql``;
|
||||
};
|
||||
|
||||
const customerListFilterSql = getCustomerListFilterSql({
|
||||
internalCustomerIds,
|
||||
inStatuses,
|
||||
plans,
|
||||
processors,
|
||||
search,
|
||||
});
|
||||
|
||||
const cursorPredicate = cursor
|
||||
? sql`AND (c.created_at, c.id) < (${cursor.createdAt}, ${cursor.id})`
|
||||
: sql``;
|
||||
|
||||
const fetchLimit = limit + 1;
|
||||
|
||||
const extraEntitlementsCTE = sql`, extra_customer_entitlements AS (
|
||||
SELECT
|
||||
cr.internal_id AS internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
),
|
||||
'rollovers', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(ro) ORDER BY ro.expires_at ASC NULLS LAST)
|
||||
FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000 OR ro.expires_at IS NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ce.id
|
||||
)
|
||||
)
|
||||
ORDER BY ce.id DESC
|
||||
) FILTER (WHERE ce.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS extra_customer_entitlements
|
||||
FROM customer_records cr
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
GROUP BY cr.internal_id
|
||||
)`;
|
||||
|
||||
return sql`
|
||||
WITH customer_records AS (
|
||||
SELECT c.*
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${orgId}
|
||||
AND c.env = ${env}
|
||||
${customerListFilterSql}
|
||||
${cursorPredicate}
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT ${fetchLimit}
|
||||
),
|
||||
|
||||
customer_products_with_prices AS (
|
||||
SELECT
|
||||
cp.*,
|
||||
row_to_json(prod) AS product,
|
||||
cpr_data.customer_prices,
|
||||
ce_data.customer_entitlements,
|
||||
ft_data.free_trial
|
||||
|
||||
FROM customer_records cr
|
||||
JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
${withStatusFilter()}
|
||||
ORDER BY (SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id) ASC, cp.created_at DESC
|
||||
LIMIT ${cusProductLimit}
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))
|
||||
) FILTER (WHERE cpr.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS customer_prices
|
||||
FROM customer_prices cpr
|
||||
LEFT JOIN prices p ON cpr.price_id = p.id
|
||||
WHERE cpr.customer_product_id = cp.id
|
||||
) cpr_data ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
),
|
||||
'rollovers', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(ro) ORDER BY ro.expires_at ASC NULLS LAST) FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000 OR ro.expires_at IS NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ce.id
|
||||
)
|
||||
)
|
||||
) FILTER (WHERE ce.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS customer_entitlements
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cp.id
|
||||
) ce_data ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT row_to_json(ft) AS free_trial
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = cp.free_trial_id
|
||||
) ft_data ON true
|
||||
),
|
||||
|
||||
customer_products_aggregated AS (
|
||||
SELECT
|
||||
cpwp.internal_customer_id,
|
||||
json_agg(row_to_json(cpwp) ORDER BY cpwp.created_at DESC) AS customer_products
|
||||
FROM customer_products_with_prices cpwp
|
||||
GROUP BY cpwp.internal_customer_id
|
||||
)
|
||||
|
||||
${
|
||||
withSubs
|
||||
? sql`, customer_subscriptions AS (
|
||||
SELECT
|
||||
s.internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(row_to_json(s)) FILTER (WHERE s.stripe_id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS subscriptions
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
cpwp.internal_customer_id,
|
||||
s.*
|
||||
FROM customer_products_with_prices cpwp
|
||||
JOIN LATERAL unnest(cpwp.subscription_ids) AS cpwp_sub(stripe_id) ON true
|
||||
JOIN subscriptions s ON s.stripe_id = cpwp_sub.stripe_id
|
||||
) s
|
||||
GROUP BY s.internal_customer_id
|
||||
)`
|
||||
: sql``
|
||||
}
|
||||
|
||||
${
|
||||
withEvents
|
||||
? sql`, customer_events AS (
|
||||
SELECT
|
||||
e.internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', e.id,
|
||||
'event_name', e.event_name,
|
||||
'value', e.value,
|
||||
'timestamp', e.timestamp,
|
||||
'properties', e.properties
|
||||
)
|
||||
ORDER BY e.timestamp DESC, e.id DESC
|
||||
) FILTER (WHERE e.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS events
|
||||
FROM events e
|
||||
WHERE e.internal_customer_id IN (SELECT internal_id FROM customer_records)
|
||||
AND e.set_usage = false
|
||||
GROUP BY e.internal_customer_id
|
||||
)`
|
||||
: sql``
|
||||
}
|
||||
|
||||
${
|
||||
withEntities
|
||||
? sql`, customer_entities AS (
|
||||
SELECT
|
||||
cr.internal_id AS internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(row_to_json(e) ORDER BY e.internal_id DESC) FILTER (WHERE e.internal_id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS entities
|
||||
FROM customer_records cr
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM entities e
|
||||
WHERE e.internal_customer_id = cr.internal_id
|
||||
ORDER BY e.internal_id DESC
|
||||
LIMIT 300
|
||||
) e ON true
|
||||
GROUP BY cr.internal_id
|
||||
)`
|
||||
: sql``
|
||||
}
|
||||
|
||||
${
|
||||
includeInvoices
|
||||
? sql`, customer_invoices AS (
|
||||
SELECT
|
||||
cr.internal_id AS internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(row_to_json(i) ORDER BY i.created_at DESC, i.id DESC) FILTER (WHERE i.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS invoices
|
||||
FROM customer_records cr
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM invoices i
|
||||
WHERE i.internal_customer_id = cr.internal_id
|
||||
ORDER BY i.created_at DESC, i.id DESC
|
||||
LIMIT 10
|
||||
) i ON true
|
||||
GROUP BY cr.internal_id
|
||||
)`
|
||||
: sql``
|
||||
}
|
||||
|
||||
${
|
||||
withTrialsUsed
|
||||
? sql`, customer_trials_used AS (
|
||||
SELECT
|
||||
cp.internal_customer_id,
|
||||
json_agg(json_build_object(
|
||||
'product_id', p.id,
|
||||
'fingerprint', c.fingerprint,
|
||||
'customer_id', c.id
|
||||
)) AS trials_used
|
||||
FROM customer_products cp
|
||||
JOIN products p ON cp.internal_product_id = p.internal_id
|
||||
JOIN customers c ON cp.internal_customer_id = c.internal_id
|
||||
WHERE cp.internal_customer_id IN (SELECT internal_id FROM customer_records)
|
||||
AND cp.free_trial_id IS NOT NULL
|
||||
GROUP BY cp.internal_customer_id
|
||||
)`
|
||||
: sql``
|
||||
}
|
||||
|
||||
${extraEntitlementsCTE}
|
||||
|
||||
SELECT
|
||||
cr.*,
|
||||
COALESCE(cpa.customer_products, '[]'::json) AS customer_products
|
||||
${withSubs ? sql`, COALESCE(cs.subscriptions, '[]'::json) AS subscriptions` : sql``}
|
||||
${withEntities ? sql`, COALESCE(ce.entities, '[]'::json) AS entities` : sql``}
|
||||
${includeInvoices ? sql`, COALESCE(ci.invoices, '[]'::json) AS invoices` : sql``}
|
||||
${withTrialsUsed ? sql`, COALESCE(ctu.trials_used, '[]'::json) AS trials_used` : sql``}
|
||||
${withEvents ? sql`, COALESCE(cev.events, '[]'::json) AS events` : sql``}
|
||||
, COALESCE(ece.extra_customer_entitlements, '[]'::json) AS extra_customer_entitlements
|
||||
FROM customer_records cr
|
||||
LEFT JOIN customer_products_aggregated cpa ON cpa.internal_customer_id = cr.internal_id
|
||||
${withSubs ? sql`LEFT JOIN customer_subscriptions cs ON cs.internal_customer_id = cr.internal_id` : sql``}
|
||||
${withEntities ? sql`LEFT JOIN customer_entities ce ON ce.internal_customer_id = cr.internal_id` : sql``}
|
||||
${includeInvoices ? sql`LEFT JOIN customer_invoices ci ON ci.internal_customer_id = cr.internal_id` : sql``}
|
||||
${withTrialsUsed ? sql`LEFT JOIN customer_trials_used ctu ON ctu.internal_customer_id = cr.internal_id` : sql``}
|
||||
${withEvents ? sql`LEFT JOIN customer_events cev ON cev.internal_customer_id = cr.internal_id` : sql``}
|
||||
LEFT JOIN extra_customer_entitlements ece ON ece.internal_customer_id = cr.internal_id
|
||||
ORDER BY cr.created_at DESC, cr.id DESC
|
||||
`;
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
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 { getOptimizedFullCusQuery } from "./optimizedFullCusQuery";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
const query = getOptimizedFullCusQuery({
|
||||
orgId: ORG_ID,
|
||||
env: AppEnv.Sandbox,
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
withSubs: false,
|
||||
limit: 1000,
|
||||
cusProductLimit: 15,
|
||||
});
|
||||
|
||||
console.log(chalk.cyan("Running optimized first page..."));
|
||||
try {
|
||||
const r = await db.execute(query);
|
||||
console.log(chalk.green(`OK: ${(r as any).length} row(s)`));
|
||||
const first = (r as any)[0];
|
||||
if (first) {
|
||||
for (const [k, v] of Object.entries(first)) {
|
||||
const arr = Array.isArray(v) ? v : v != null && typeof v === "object" && "length" in (v as any) ? (v as any[]) : null;
|
||||
console.log(` ${k}: ${arr ? `${arr.length} items` : typeof v}`);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(chalk.red(`SQL ERROR: ${e.message}`));
|
||||
console.error(chalk.gray(`code: ${e.code}, where: ${e.where ?? "n/a"}, detail: ${e.detail ?? "n/a"}, hint: ${e.hint ?? "n/a"}`));
|
||||
if (e.position) console.error(chalk.gray(`position: ${e.position}`));
|
||||
}
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,305 +0,0 @@
|
||||
import {
|
||||
AppEnv,
|
||||
type FullCustomer,
|
||||
RELEVANT_STATUSES,
|
||||
StandardCursor,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { CusBatchService } from "../src/internal/customers/CusBatchService";
|
||||
import { CusSearchService } from "../src/internal/customers/CusSearchService";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import {
|
||||
type FlattenedCustomerRow,
|
||||
reassembleFlattenedCustomer,
|
||||
} from "../src/internal/customers/reassembleFlattenedCustomer";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
const ENV = AppEnv.Sandbox;
|
||||
|
||||
type DashboardFilters = {
|
||||
status?: string[];
|
||||
version?: string[];
|
||||
none?: boolean;
|
||||
processor?: string[];
|
||||
};
|
||||
|
||||
const runOldPath = async ({
|
||||
ctx,
|
||||
search,
|
||||
filters,
|
||||
cursor,
|
||||
limit,
|
||||
}: {
|
||||
ctx: any;
|
||||
search: string;
|
||||
filters?: DashboardFilters;
|
||||
cursor: { t: number; id: string } | null;
|
||||
limit: number;
|
||||
}): Promise<{
|
||||
fullCustomers: FullCustomer[];
|
||||
next_cursor: string | null;
|
||||
}> => {
|
||||
const { internalIds, peek } =
|
||||
await CusSearchService.resolveInternalIdsByCursor({
|
||||
db: ctx.db,
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
search,
|
||||
filters,
|
||||
cursor,
|
||||
limit,
|
||||
});
|
||||
if (internalIds.length === 0) {
|
||||
return { fullCustomers: [], next_cursor: null };
|
||||
}
|
||||
const query = getCursorPaginatedFullCusQuery({
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
withSubs: true,
|
||||
limit: internalIds.length,
|
||||
internalCustomerIds: internalIds,
|
||||
cusProductLimit: 15,
|
||||
});
|
||||
const rows = (await ctx.db.execute(query)) as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
const flat = (rows[0] ?? {
|
||||
customers: [],
|
||||
customer_products: [],
|
||||
customer_entitlements: [],
|
||||
extra_customer_entitlements: [],
|
||||
customer_prices: [],
|
||||
entitlements: [],
|
||||
rollovers: [],
|
||||
replaceables: [],
|
||||
free_trials: [],
|
||||
subscriptions: [],
|
||||
}) as unknown as FlattenedCustomerRow;
|
||||
const fullCustomers = reassembleFlattenedCustomer(flat);
|
||||
const next_cursor = peek ? StandardCursor.encode(peek) : null;
|
||||
return { fullCustomers, next_cursor };
|
||||
};
|
||||
|
||||
const minimalCtx = () => {
|
||||
const { db, client } = initDrizzle();
|
||||
return {
|
||||
db,
|
||||
client,
|
||||
org: { id: ORG_ID, slug: "unit-test-org" },
|
||||
env: ENV,
|
||||
logger: {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
warn: () => {},
|
||||
debug: () => {},
|
||||
trace: () => {},
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
|
||||
const sortById = <T extends { internal_id?: string; id?: string | null }>(
|
||||
arr: T[],
|
||||
): T[] =>
|
||||
[...arr].sort((a, b) =>
|
||||
(a.internal_id ?? a.id ?? "").localeCompare(b.internal_id ?? 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 any[]).sort((a, b) => {
|
||||
const ka = (a as any).id ?? (a as any).internal_id ?? "";
|
||||
const kb = (b as any).id ?? (b as any).internal_id ?? "";
|
||||
return String(ka).localeCompare(String(kb));
|
||||
});
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
if (obj && typeof obj === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj as Record<string, unknown>)
|
||||
.map(([k, v]) => [k, sortDeep(v)] as const)
|
||||
.sort(([a], [b]) => a.localeCompare(b)),
|
||||
);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
const diffCustomers = (
|
||||
a: FullCustomer[],
|
||||
b: FullCustomer[],
|
||||
label: string,
|
||||
) => {
|
||||
if (a.length !== b.length) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
` ${label} ✗ length mismatch: old=${a.length} new=${b.length}`,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const sa = sortById(a);
|
||||
const sb = sortById(b);
|
||||
let mismatches = 0;
|
||||
for (let i = 0; i < sa.length; i++) {
|
||||
const ja = JSON.stringify(sortDeep(sa[i]));
|
||||
const jb = JSON.stringify(sortDeep(sb[i]));
|
||||
if (ja !== jb) {
|
||||
if (mismatches < 2) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
` ${label} ✗ customer #${i} (${(sa[i] as any).id}) differs`,
|
||||
),
|
||||
);
|
||||
const keysA = Object.keys(sortDeep(sa[i]) as object);
|
||||
for (const k of keysA) {
|
||||
const va = JSON.stringify((sortDeep(sa[i]) as any)[k]);
|
||||
const vb = JSON.stringify((sortDeep(sb[i]) as any)[k]);
|
||||
if (va !== vb) {
|
||||
console.log(
|
||||
chalk.gray(` KEY ${k}\n old=${va?.slice(0, 160)}\n new=${vb?.slice(0, 160)}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
if (mismatches > 0) {
|
||||
console.log(
|
||||
chalk.red(` ${label} ✗ ${mismatches}/${sa.length} customers differ`),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
console.log(chalk.green(` ${label} ✓ ${sa.length} customers identical`));
|
||||
return true;
|
||||
};
|
||||
|
||||
type Case = {
|
||||
name: string;
|
||||
search?: string;
|
||||
filters?: DashboardFilters;
|
||||
cursor?: { t: number; id: string } | null;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Dashboard Merge Parity ================\n",
|
||||
),
|
||||
);
|
||||
|
||||
const ctx = minimalCtx();
|
||||
|
||||
const total = (
|
||||
(await ctx.db.execute(
|
||||
sql`SELECT COUNT(*)::int AS n FROM customers WHERE org_id = ${ORG_ID} AND env = ${ENV}`,
|
||||
)) as unknown as { n: number }[]
|
||||
)[0].n;
|
||||
const deepOffset = Math.floor(total * 0.95);
|
||||
const deepRow = (await ctx.db.execute(
|
||||
sql`SELECT created_at, id FROM customers WHERE org_id = ${ORG_ID} AND env = ${ENV} ORDER BY created_at DESC, id DESC LIMIT 1 OFFSET ${deepOffset}`,
|
||||
)) as unknown as { created_at: number; id: string }[];
|
||||
const deepCursor = {
|
||||
t: Number(deepRow[0].created_at),
|
||||
id: deepRow[0].id,
|
||||
};
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` total=${total.toLocaleString()} deep_cursor=${deepCursor.id} (offset ~95%)\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const cases: Case[] = [
|
||||
{ name: "baseline", limit: 200 },
|
||||
{ name: "search 'bench_000450'", search: "bench_000450", limit: 100 },
|
||||
{ name: "status=active", filters: { status: ["active"] }, limit: 200 },
|
||||
{ name: "status=past_due", filters: { status: ["past_due"] }, limit: 200 },
|
||||
{ name: "status=canceled", filters: { status: ["canceled"] }, limit: 200 },
|
||||
{
|
||||
name: "status=free_trial",
|
||||
filters: { status: ["free_trial"] },
|
||||
limit: 200,
|
||||
},
|
||||
{ name: "status=expired", filters: { status: ["expired"] }, limit: 200 },
|
||||
{
|
||||
name: "status=active,past_due",
|
||||
filters: { status: ["active", "past_due"] },
|
||||
limit: 200,
|
||||
},
|
||||
{ name: "none=true", filters: { none: true }, limit: 200 },
|
||||
{
|
||||
name: "processor=stripe",
|
||||
filters: { processor: ["stripe"] },
|
||||
limit: 200,
|
||||
},
|
||||
{ name: "deep cursor (95%)", cursor: deepCursor, limit: 200 },
|
||||
{
|
||||
name: "deep cursor + status=active",
|
||||
cursor: deepCursor,
|
||||
filters: { status: ["active"] },
|
||||
limit: 200,
|
||||
},
|
||||
{
|
||||
name: "search no-match",
|
||||
search: "no_such_customer_zzz",
|
||||
limit: 50,
|
||||
},
|
||||
];
|
||||
|
||||
let pass = 0;
|
||||
let fail = 0;
|
||||
try {
|
||||
for (const tc of cases) {
|
||||
const args = {
|
||||
ctx,
|
||||
search: tc.search ?? "",
|
||||
filters: tc.filters,
|
||||
cursor: tc.cursor ?? null,
|
||||
limit: tc.limit ?? 200,
|
||||
};
|
||||
const oldRes = await runOldPath(args);
|
||||
const newRes = await CusBatchService.getDashboardCursorPage(args);
|
||||
const ok = diffCustomers(
|
||||
oldRes.fullCustomers,
|
||||
newRes.fullCustomers,
|
||||
tc.name,
|
||||
);
|
||||
const oldDecoded = StandardCursor.decode(oldRes.next_cursor ?? "");
|
||||
const newDecoded = StandardCursor.decode(newRes.next_cursor ?? "");
|
||||
const cursorMatches =
|
||||
(oldDecoded?.t ?? null) === (newDecoded?.t ?? null) &&
|
||||
(oldDecoded?.id ?? null) === (newDecoded?.id ?? null);
|
||||
if (!cursorMatches) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
` cursor differs: old=${JSON.stringify(oldDecoded)} new=${JSON.stringify(newDecoded)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (ok && cursorMatches) pass++;
|
||||
else fail++;
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.bold(
|
||||
`Result: ${chalk.green(`${pass} pass`)} / ${chalk.red(`${fail} fail`)} of ${cases.length}`,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await ctx.client.end();
|
||||
process.exit(fail === 0 ? 0 : 1);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,180 +0,0 @@
|
||||
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();
|
||||
@@ -1,426 +0,0 @@
|
||||
import {
|
||||
type AppEnv,
|
||||
type CusProductStatus,
|
||||
type FullCustomer,
|
||||
type ListCustomersV2Params,
|
||||
RELEVANT_STATUSES,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
import { getPaginatedFullCusQuery } from "../src/internal/customers/getFullCusQuery";
|
||||
import {
|
||||
type FlattenedCustomerRow,
|
||||
reassembleFlattenedCustomer,
|
||||
} from "../src/internal/customers/reassembleFlattenedCustomer";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORGS: { id: string; env: AppEnv; label: string }[] = [
|
||||
{ id: "biu9vSF7vghBLSKW1UTDwxHBAivjnPaK", env: "live" as AppEnv, label: "firecrawl" },
|
||||
{ id: "r7pwHiekGsqt32qGqcVku6thWFh5aHh8", env: "live" as AppEnv, label: "runable" },
|
||||
];
|
||||
const LIMIT = parseInt(process.env.DIFF_LIMIT ?? "50", 10);
|
||||
const DEEP_OFFSET = parseInt(process.env.DIFF_DEEP_OFFSET ?? "500", 10);
|
||||
const CUS_PRODUCT_LIMIT = 15;
|
||||
const STATEMENT_TIMEOUT_MS = 30_000;
|
||||
|
||||
type ResolvedParams = Partial<ListCustomersV2Params> & {
|
||||
offset?: number;
|
||||
cursor?: { t: number; id: string };
|
||||
};
|
||||
|
||||
type CaseContext = { db: DB; orgId: string; env: AppEnv };
|
||||
type Case = {
|
||||
name: string;
|
||||
build: (ctx: CaseContext) => Promise<ResolvedParams | null>;
|
||||
};
|
||||
|
||||
type DB = ReturnType<typeof initDrizzle>["db"];
|
||||
|
||||
const runReadOnly = async <T,>(db: DB, fn: (tx: any) => Promise<T>): Promise<T> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
await tx.execute(
|
||||
sql.raw(`SET LOCAL statement_timeout = ${STATEMENT_TIMEOUT_MS}`),
|
||||
);
|
||||
await tx.execute(sql.raw("SET LOCAL transaction_read_only = on"));
|
||||
return fn(tx);
|
||||
});
|
||||
};
|
||||
|
||||
const v22NormalizeTimestamp = (value: any): number => {
|
||||
if (typeof value === "string") {
|
||||
const parsed = parseInt(value, 10);
|
||||
return Number.isNaN(parsed) ? Date.now() : parsed;
|
||||
}
|
||||
return typeof value === "number" ? value : Date.now();
|
||||
};
|
||||
|
||||
const v22Normalize = (raw: any): any => {
|
||||
const out = {
|
||||
...raw,
|
||||
created_at: v22NormalizeTimestamp(raw.created_at),
|
||||
};
|
||||
if (raw.customer_products && Array.isArray(raw.customer_products)) {
|
||||
out.customer_products = raw.customer_products.map((cp: any) => ({
|
||||
...cp,
|
||||
created_at: v22NormalizeTimestamp(cp.created_at),
|
||||
starts_at: cp.starts_at
|
||||
? v22NormalizeTimestamp(cp.starts_at)
|
||||
: v22NormalizeTimestamp(cp.created_at),
|
||||
canceled_at: cp.canceled_at ? v22NormalizeTimestamp(cp.canceled_at) : null,
|
||||
ended_at: cp.ended_at ? v22NormalizeTimestamp(cp.ended_at) : null,
|
||||
trial_ends_at: cp.trial_ends_at
|
||||
? v22NormalizeTimestamp(cp.trial_ends_at)
|
||||
: null,
|
||||
quantity: cp.quantity ? parseInt(cp.quantity, 10) || 1 : 1,
|
||||
options: cp.options || [],
|
||||
collection_method: cp.collection_method || "charge_automatically",
|
||||
subscription_ids: cp.subscription_ids || [],
|
||||
scheduled_ids: cp.scheduled_ids || [],
|
||||
customer_entitlements: (cp.customer_entitlements || []).map((ce: any) => ({
|
||||
...ce,
|
||||
created_at: v22NormalizeTimestamp(ce.created_at),
|
||||
next_reset_at: ce.next_reset_at
|
||||
? v22NormalizeTimestamp(ce.next_reset_at)
|
||||
: null,
|
||||
balance: ce.balance ? parseFloat(ce.balance) || 0 : 0,
|
||||
adjustment: ce.adjustment ? parseFloat(ce.adjustment) || 0 : 0,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const fetchV22 = async (
|
||||
db: DB,
|
||||
orgId: string,
|
||||
env: AppEnv,
|
||||
params: ResolvedParams,
|
||||
) => {
|
||||
const { subscription_status, plans, search, processors, offset = 0 } = params;
|
||||
const query = getPaginatedFullCusQuery({
|
||||
orgId,
|
||||
env,
|
||||
inStatuses: subscription_status
|
||||
? [subscription_status as unknown as CusProductStatus]
|
||||
: RELEVANT_STATUSES,
|
||||
includeInvoices: false,
|
||||
withEntities: false,
|
||||
withTrialsUsed: false,
|
||||
withSubs: true,
|
||||
limit: LIMIT,
|
||||
offset,
|
||||
search,
|
||||
plans,
|
||||
processors,
|
||||
cusProductLimit: CUS_PRODUCT_LIMIT,
|
||||
});
|
||||
const rows = await runReadOnly(db, (tx) => tx.execute(query));
|
||||
return (rows as any[]).map(v22Normalize) as FullCustomer[];
|
||||
};
|
||||
|
||||
const fetchV23 = async (
|
||||
db: DB,
|
||||
orgId: string,
|
||||
env: AppEnv,
|
||||
params: ResolvedParams,
|
||||
) => {
|
||||
const { subscription_status, plans, search, processors, cursor } = params;
|
||||
const query = getCursorPaginatedFullCusQuery({
|
||||
orgId,
|
||||
env,
|
||||
inStatuses: subscription_status
|
||||
? [subscription_status as unknown as CusProductStatus]
|
||||
: RELEVANT_STATUSES,
|
||||
withSubs: true,
|
||||
limit: LIMIT,
|
||||
search,
|
||||
plans,
|
||||
processors,
|
||||
cursor: cursor ? { v: 0 as const, t: cursor.t, id: cursor.id } : undefined,
|
||||
cusProductLimit: CUS_PRODUCT_LIMIT,
|
||||
});
|
||||
const rows = (await runReadOnly(db, (tx) => tx.execute(query))) as any[];
|
||||
const flat = (rows[0] ?? {}) as unknown as FlattenedCustomerRow;
|
||||
const all = reassembleFlattenedCustomer(flat);
|
||||
return all.slice(0, LIMIT);
|
||||
};
|
||||
|
||||
const resolveCursorAtOffset = async ({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
offset,
|
||||
inStatuses,
|
||||
plans,
|
||||
processors,
|
||||
search,
|
||||
}: {
|
||||
db: DB;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
offset: number;
|
||||
inStatuses?: CusProductStatus[];
|
||||
plans?: ListCustomersV2Params["plans"];
|
||||
processors?: ListCustomersV2Params["processors"];
|
||||
search?: string;
|
||||
}): Promise<{ t: number; id: string } | null> => {
|
||||
const query = getPaginatedFullCusQuery({
|
||||
orgId,
|
||||
env,
|
||||
inStatuses: inStatuses ?? RELEVANT_STATUSES,
|
||||
includeInvoices: false,
|
||||
withEntities: false,
|
||||
withTrialsUsed: false,
|
||||
withSubs: false,
|
||||
limit: 1,
|
||||
offset: offset - 1,
|
||||
search,
|
||||
plans,
|
||||
processors,
|
||||
cusProductLimit: 1,
|
||||
});
|
||||
const rows = (await runReadOnly(db, (tx) => tx.execute(query))) as any[];
|
||||
if (rows.length === 0) return null;
|
||||
const row = rows[0] as { id: string; created_at: number | string };
|
||||
return { t: v22NormalizeTimestamp(row.created_at), id: row.id };
|
||||
};
|
||||
|
||||
const resolveFirstPlanId = async ({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
db: DB;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}): Promise<string | null> => {
|
||||
const rows = (await runReadOnly(
|
||||
db,
|
||||
(tx) => tx.execute(sql`
|
||||
SELECT p.id
|
||||
FROM products p
|
||||
JOIN customer_products cp ON cp.internal_product_id = p.internal_id
|
||||
WHERE p.org_id = ${orgId} AND p.env = ${env}
|
||||
GROUP BY p.id
|
||||
ORDER BY COUNT(*) DESC
|
||||
LIMIT 1
|
||||
`),
|
||||
)) as any[];
|
||||
return rows[0]?.id ?? null;
|
||||
};
|
||||
|
||||
const resolveSearchSubstring = async ({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
db: DB;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}): Promise<string | null> => {
|
||||
const rows = (await runReadOnly(
|
||||
db,
|
||||
(tx) => tx.execute(sql`
|
||||
SELECT c.email
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${orgId} AND c.env = ${env}
|
||||
AND c.email IS NOT NULL
|
||||
AND length(c.email) >= 5
|
||||
LIMIT 1
|
||||
`),
|
||||
)) as any[];
|
||||
const email = rows[0]?.email as string | undefined;
|
||||
if (!email) return null;
|
||||
const at = email.indexOf("@");
|
||||
if (at < 3) return email.slice(0, 3);
|
||||
return email.slice(0, Math.min(at, 4));
|
||||
};
|
||||
|
||||
const CASES: Case[] = [
|
||||
{
|
||||
name: "page 1 / no filters",
|
||||
build: async () => ({}),
|
||||
},
|
||||
{
|
||||
name: "page 1 / subscription_status=active",
|
||||
build: async () => ({ subscription_status: "active" }),
|
||||
},
|
||||
{
|
||||
name: "page 1 / processors=revenuecat",
|
||||
build: async () => ({ processors: ["revenuecat"] }),
|
||||
},
|
||||
{
|
||||
name: "page 1 / search",
|
||||
build: async (ctx) => {
|
||||
const search = await resolveSearchSubstring(ctx);
|
||||
if (!search) return null;
|
||||
return { search };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "page 1 / plans",
|
||||
build: async (ctx) => {
|
||||
const planId = await resolveFirstPlanId(ctx);
|
||||
if (!planId) return null;
|
||||
return { plans: [{ id: planId }] };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `deep / offset=${DEEP_OFFSET} / no filters`,
|
||||
build: async (ctx) => {
|
||||
const cursor = await resolveCursorAtOffset({
|
||||
db: ctx.db,
|
||||
orgId: ctx.orgId,
|
||||
env: ctx.env,
|
||||
offset: DEEP_OFFSET,
|
||||
});
|
||||
if (!cursor) return null;
|
||||
return { offset: DEEP_OFFSET, cursor };
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const findDiff = (a: any, b: any, path = "$"): string | null => {
|
||||
if (a === b) return null;
|
||||
if (a === null || b === null || a === undefined || b === undefined) {
|
||||
return `${path}: ${JSON.stringify(a)} vs ${JSON.stringify(b)}`;
|
||||
}
|
||||
if (typeof a !== typeof b) {
|
||||
return `${path}: type ${typeof a} vs ${typeof b}`;
|
||||
}
|
||||
if (Array.isArray(a) || Array.isArray(b)) {
|
||||
if (!Array.isArray(a) || !Array.isArray(b)) {
|
||||
return `${path}: array vs non-array`;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return `${path}.length: ${a.length} vs ${b.length}`;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const d = findDiff(a[i], b[i], `${path}[${i}]`);
|
||||
if (d) return d;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (typeof a === "object") {
|
||||
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
||||
for (const k of keys) {
|
||||
const d = findDiff(a[k], b[k], `${path}.${k}`);
|
||||
if (d) return d;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return `${path}: ${JSON.stringify(a)} vs ${JSON.stringify(b)}`;
|
||||
};
|
||||
|
||||
const summarize = (label: string, list: FullCustomer[]) => {
|
||||
const cps = list.reduce((n, c) => n + (c.customer_products?.length ?? 0), 0);
|
||||
const subs = list.reduce(
|
||||
(n, c) => n + (c.subscriptions?.length ?? 0),
|
||||
0,
|
||||
);
|
||||
const extras = list.reduce(
|
||||
(n, c) => n + (c.extra_customer_entitlements?.length ?? 0),
|
||||
0,
|
||||
);
|
||||
return `${label}: ${list.length} cus / ${cps} cps / ${subs} subs / ${extras} loose_ces`;
|
||||
};
|
||||
|
||||
const runOrg = async ({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
label,
|
||||
}: {
|
||||
db: DB;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
label: string;
|
||||
}) => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n========= ${label} (${orgId}) env=${env} limit=${LIMIT} =========\n`,
|
||||
),
|
||||
);
|
||||
let total = 0;
|
||||
let passes = 0;
|
||||
for (const c of CASES) {
|
||||
total++;
|
||||
console.log(chalk.cyan(`\n--- ${c.name} ---`));
|
||||
const params = await c.build({ db, orgId, env });
|
||||
if (!params) {
|
||||
console.log(chalk.yellow(" ⊘ skipped (no data to build params)"));
|
||||
passes++;
|
||||
continue;
|
||||
}
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` params: ${JSON.stringify(params, (_k, v) => (typeof v === "bigint" ? String(v) : v))}`,
|
||||
),
|
||||
);
|
||||
const [v22, v23] = await Promise.all([
|
||||
fetchV22(db, orgId, env, params),
|
||||
fetchV23(db, orgId, env, params),
|
||||
]);
|
||||
console.log(chalk.gray(` ${summarize("V2.2", v22)}`));
|
||||
console.log(chalk.gray(` ${summarize("V2.3", v23)}`));
|
||||
const diff = findDiff(v22, v23);
|
||||
if (diff) {
|
||||
console.log(chalk.red(` ❌ DIFF at ${diff}`));
|
||||
const idx = diff.match(/\$\[(\d+)\]/);
|
||||
if (idx) {
|
||||
const i = parseInt(idx[1]!, 10);
|
||||
console.log(chalk.gray(` --- V2.2 [${i}] ---`));
|
||||
console.log(chalk.gray(JSON.stringify(v22[i], null, 2).slice(0, 4000)));
|
||||
console.log(chalk.gray(` --- V2.3 [${i}] ---`));
|
||||
console.log(chalk.gray(JSON.stringify(v23[i], null, 2).slice(0, 4000)));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.green(` ✓ MATCH`));
|
||||
passes++;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
chalk.magentaBright(`\n ${label}: ${passes}/${total} cases match`),
|
||||
);
|
||||
return { total, passes };
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
let grandTotal = 0;
|
||||
let grandPasses = 0;
|
||||
for (const org of ORGS) {
|
||||
try {
|
||||
const { total, passes } = await runOrg({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env: org.env,
|
||||
label: org.label,
|
||||
});
|
||||
grandTotal += total;
|
||||
grandPasses += passes;
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`\n❌ Org ${org.label} failed:`), err);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n========= TOTAL: ${grandPasses}/${grandTotal} cases match =========\n`,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red("\n❌ Diff failed:"));
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,52 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
const tables = [
|
||||
"customers",
|
||||
"customer_products",
|
||||
"customer_entitlements",
|
||||
"customer_prices",
|
||||
"subscriptions",
|
||||
];
|
||||
for (const t of tables) {
|
||||
console.log(chalk.bold(`\n=== ${t} indexes ===`));
|
||||
const r = (await db.execute(sql.raw(`
|
||||
SELECT indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE tablename = '${t}' AND schemaname = 'public'
|
||||
ORDER BY indexname
|
||||
`))) as unknown as { indexname: string; indexdef: string }[];
|
||||
for (const idx of r) {
|
||||
console.log(` ${idx.indexname}`);
|
||||
console.log(` ${idx.indexdef}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get total rows in customer_products across all orgs
|
||||
console.log(chalk.bold("\n=== Global customer_products stats ==="));
|
||||
const stats = (await db.execute(sql.raw(`
|
||||
SELECT
|
||||
reltuples::bigint AS estimated_rows,
|
||||
pg_size_pretty(pg_relation_size('customer_products')) AS table_size,
|
||||
pg_size_pretty(pg_indexes_size('customer_products')) AS indexes_size
|
||||
FROM pg_class WHERE relname = 'customer_products'
|
||||
`))) as unknown as { estimated_rows: string; table_size: string; indexes_size: string }[];
|
||||
console.log(` estimated_rows=${stats[0].estimated_rows} table=${stats[0].table_size} indexes=${stats[0].indexes_size}`);
|
||||
|
||||
// Actual count globally
|
||||
const real = (await db.execute(sql.raw(`SELECT COUNT(*)::int AS n FROM customer_products`))) as unknown as { n: number }[];
|
||||
console.log(` actual_total_rows=${real[0].n.toLocaleString()}`);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,90 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
const tables = [
|
||||
"customers",
|
||||
"customer_products",
|
||||
"customer_entitlements",
|
||||
"customer_prices",
|
||||
"subscriptions",
|
||||
"entitlements",
|
||||
"products",
|
||||
"rollovers",
|
||||
"replaceables",
|
||||
"free_trials",
|
||||
];
|
||||
|
||||
console.log(chalk.bold(`\nRow counts for org=${ORG_ID}:\n`));
|
||||
for (const t of tables) {
|
||||
const hasOrg = ["customers", "subscriptions", "entitlements", "products", "features"];
|
||||
const col = hasOrg.includes(t)
|
||||
? `WHERE org_id = '${ORG_ID}'`
|
||||
: t === "customer_products" ||
|
||||
t === "customer_entitlements" ||
|
||||
t === "customer_prices"
|
||||
? `WHERE internal_customer_id IN (SELECT internal_id FROM customers WHERE org_id = '${ORG_ID}')`
|
||||
: "";
|
||||
const start = performance.now();
|
||||
const r = (await db.execute(
|
||||
sql.raw(`SELECT COUNT(*)::int AS n FROM ${t} ${col}`),
|
||||
)) as unknown as { n: number }[];
|
||||
const n = r[0]?.n ?? 0;
|
||||
console.log(
|
||||
` ${t.padEnd(28)} ${n.toLocaleString().padStart(10)} (${(performance.now() - start).toFixed(0)}ms)`,
|
||||
);
|
||||
}
|
||||
|
||||
// Sample a customer to see if it has any products
|
||||
const sample = (await db.execute(sql.raw(`
|
||||
SELECT c.internal_id, c.id,
|
||||
(SELECT COUNT(*)::int FROM customer_products cp WHERE cp.internal_customer_id = c.internal_id) AS cp_count,
|
||||
(SELECT COUNT(*)::int FROM customer_entitlements ce WHERE ce.internal_customer_id = c.internal_id) AS ce_count
|
||||
FROM customers c
|
||||
WHERE c.org_id = '${ORG_ID}'
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT 10
|
||||
`))) as unknown as Record<string, unknown>[];
|
||||
console.log(chalk.bold(`\nFirst 10 customers by created_at DESC:\n`));
|
||||
for (const row of sample) {
|
||||
console.log(
|
||||
` ${(row.id as string).padEnd(30)} cp=${row.cp_count} ce=${row.ce_count}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Histogram: how many customers have N products?
|
||||
const hist = (await db.execute(sql.raw(`
|
||||
SELECT cp_bucket, COUNT(*)::int AS customer_count FROM (
|
||||
SELECT c.internal_id,
|
||||
LEAST(
|
||||
(SELECT COUNT(*)::int FROM customer_products cp WHERE cp.internal_customer_id = c.internal_id),
|
||||
10
|
||||
) AS cp_bucket
|
||||
FROM customers c
|
||||
WHERE c.org_id = '${ORG_ID}'
|
||||
) t
|
||||
GROUP BY cp_bucket
|
||||
ORDER BY cp_bucket
|
||||
`))) as unknown as { cp_bucket: number; customer_count: number }[];
|
||||
console.log(chalk.bold(`\nCustomer × #products histogram (capped at 10):\n`));
|
||||
for (const row of hist) {
|
||||
const bar = "█".repeat(Math.min(60, Math.floor(row.customer_count / 5000)));
|
||||
console.log(
|
||||
` ${row.cp_bucket.toString().padStart(2)}: ${row.customer_count.toLocaleString().padStart(8)} ${bar}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,173 +0,0 @@
|
||||
import type {
|
||||
AppEnv,
|
||||
CusProductStatus,
|
||||
ListCustomersV2Params,
|
||||
StandardCursorFields,
|
||||
} from "@autumn/shared";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { getCustomerListFilterSql } from "../src/internal/customers/getFullCusQuery";
|
||||
|
||||
export type OptimizedQueryArgs = {
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
inStatuses?: CusProductStatus[];
|
||||
withSubs?: boolean;
|
||||
limit: number;
|
||||
cursor?: StandardCursorFields;
|
||||
internalCustomerIds?: string[];
|
||||
plans?: ListCustomersV2Params["plans"];
|
||||
processors?: ListCustomersV2Params["processors"];
|
||||
search?: string;
|
||||
cusProductLimit: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set-based replacement for the per-customer LATERAL nested loops.
|
||||
* Key change: each child fetch is a single hash/merge join over the cr CTE
|
||||
* instead of 1001 sequential scans (original Seq Scan-per-loop pattern).
|
||||
*/
|
||||
export const getOptimizedFullCusQuery = ({
|
||||
orgId,
|
||||
env,
|
||||
inStatuses,
|
||||
withSubs = true,
|
||||
limit,
|
||||
cursor,
|
||||
internalCustomerIds,
|
||||
plans,
|
||||
processors,
|
||||
search,
|
||||
cusProductLimit,
|
||||
}: OptimizedQueryArgs) => {
|
||||
const cpStatusFilter = inStatuses?.length
|
||||
? sql`AND cp.status = ANY(ARRAY[${sql.join(
|
||||
inStatuses.map((status) => sql`${status}`),
|
||||
sql`, `,
|
||||
)}])`
|
||||
: sql``;
|
||||
|
||||
const customerListFilterSql = getCustomerListFilterSql({
|
||||
internalCustomerIds,
|
||||
inStatuses,
|
||||
plans,
|
||||
processors,
|
||||
search,
|
||||
});
|
||||
|
||||
const cursorPredicate = cursor
|
||||
? sql`AND (c.created_at, c.id) < (${cursor.t}, ${cursor.id})`
|
||||
: sql``;
|
||||
|
||||
const fetchLimit = limit + 1;
|
||||
|
||||
const subscriptionsSelect = withSubs
|
||||
? sql`(
|
||||
SELECT COALESCE(json_agg(row_to_json(s)), '[]'::json)
|
||||
FROM (
|
||||
SELECT DISTINCT s.*
|
||||
FROM cps_ranked cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN subscriptions s ON s.stripe_id = sub_id_t.sub_id
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
) s
|
||||
) AS subscriptions`
|
||||
: sql`'[]'::json AS subscriptions`;
|
||||
|
||||
return sql`
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT
|
||||
c.internal_id,
|
||||
c.id,
|
||||
c.created_at,
|
||||
row_to_json(c) AS row_json
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${orgId}
|
||||
AND c.env = ${env}
|
||||
${customerListFilterSql}
|
||||
${cursorPredicate}
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT ${fetchLimit}
|
||||
),
|
||||
cp_ranked_ids AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.id,
|
||||
cp.internal_customer_id,
|
||||
cp.internal_product_id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY cp.internal_customer_id
|
||||
ORDER BY prod.is_add_on ASC, cp.created_at DESC
|
||||
) AS rn
|
||||
FROM cr
|
||||
JOIN customer_products cp ON cp.internal_customer_id = cr.internal_id
|
||||
JOIN products prod ON prod.internal_id = cp.internal_product_id
|
||||
WHERE TRUE ${cpStatusFilter}
|
||||
),
|
||||
cps_ranked AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.id,
|
||||
cp.internal_customer_id,
|
||||
cp.internal_product_id,
|
||||
cp.free_trial_id,
|
||||
cp.subscription_ids,
|
||||
(row_to_json(cp)::jsonb || jsonb_build_object('product', row_to_json(prod)))::json AS row_json
|
||||
FROM cp_ranked_ids r
|
||||
JOIN customer_products cp ON cp.id = r.id
|
||||
JOIN products prod ON prod.internal_id = r.internal_product_id
|
||||
WHERE r.rn <= ${cusProductLimit}
|
||||
),
|
||||
ces_bound AS MATERIALIZED (
|
||||
-- Single hash join instead of 1001 LATERAL scans.
|
||||
SELECT ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cps_ranked
|
||||
JOIN customer_entitlements ce ON ce.customer_product_id = cps_ranked.id
|
||||
),
|
||||
ces_loose AS MATERIALIZED (
|
||||
-- LATERAL with LIMIT 30 stays; existing partial index is well-suited.
|
||||
SELECT ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
ces_all AS MATERIALIZED (
|
||||
SELECT id, entitlement_id FROM ces_bound
|
||||
UNION ALL
|
||||
SELECT id, entitlement_id FROM ces_loose
|
||||
)
|
||||
SELECT
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cr) AS customers,
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cps_ranked) AS customer_products,
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM ces_bound) AS customer_entitlements,
|
||||
(SELECT COALESCE(json_agg(row_json ORDER BY id DESC), '[]'::json) FROM ces_loose) AS extra_customer_entitlements,
|
||||
(SELECT COALESCE(json_agg(row_to_json(cpr)::jsonb || jsonb_build_object('price', row_to_json(p))), '[]'::json)
|
||||
FROM cps_ranked
|
||||
JOIN customer_prices cpr ON cpr.customer_product_id = cps_ranked.id
|
||||
LEFT JOIN prices p ON p.id = cpr.price_id
|
||||
) AS customer_prices,
|
||||
(SELECT COALESCE(json_agg(row_to_json(e)::jsonb || jsonb_build_object('feature', row_to_json(f))), '[]'::json)
|
||||
FROM (SELECT DISTINCT entitlement_id FROM ces_all) ce
|
||||
JOIN entitlements e ON e.id = ce.entitlement_id
|
||||
JOIN features f ON f.internal_id = e.internal_feature_id
|
||||
) AS entitlements,
|
||||
(SELECT COALESCE(json_agg(row_to_json(ro)), '[]'::json)
|
||||
FROM ces_all
|
||||
JOIN rollovers ro ON ro.cus_ent_id = ces_all.id
|
||||
WHERE ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000
|
||||
) AS rollovers,
|
||||
(SELECT COALESCE(json_agg(row_to_json(r)), '[]'::json)
|
||||
FROM ces_all
|
||||
JOIN replaceables r ON r.cus_ent_id = ces_all.id
|
||||
) AS replaceables,
|
||||
(SELECT COALESCE(json_agg(row_to_json(ft)), '[]'::json)
|
||||
FROM (SELECT DISTINCT free_trial_id FROM cps_ranked WHERE free_trial_id IS NOT NULL) cps
|
||||
JOIN free_trials ft ON ft.id = cps.free_trial_id
|
||||
) AS free_trials,
|
||||
${subscriptionsSelect}
|
||||
`;
|
||||
};
|
||||
@@ -1,368 +0,0 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql, type SQL } from "drizzle-orm";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
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";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "r7pwHiekGsqt32qGqcVku6thWFh5aHh8";
|
||||
const ENV: AppEnv = AppEnv.Live;
|
||||
const LABEL = "runable";
|
||||
const REPEATS = 5;
|
||||
const STATEMENT_TIMEOUT_MS = 30_000;
|
||||
const DEEP_OFFSET_PCT = 45;
|
||||
const BASE_LIMIT = 1000;
|
||||
const READ_ONLY = true;
|
||||
const CUS_PRODUCT_LIMIT = 15;
|
||||
|
||||
const RESULTS_DIR = join(import.meta.dir, "results");
|
||||
|
||||
type DB = ReturnType<typeof initDrizzle>["db"];
|
||||
|
||||
const sharedFullCusOpts = {
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
includeInvoices: false,
|
||||
withEntities: false,
|
||||
withTrialsUsed: false,
|
||||
withSubs: true,
|
||||
cusProductLimit: CUS_PRODUCT_LIMIT,
|
||||
};
|
||||
|
||||
type Cell = {
|
||||
name: string;
|
||||
build: () => SQL;
|
||||
includeExplain?: boolean;
|
||||
};
|
||||
|
||||
type CellResult = {
|
||||
name: string;
|
||||
medianMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
rowCount: number;
|
||||
samples: number[];
|
||||
explainPlan?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const normalizeRows = (r: unknown): Record<string, unknown>[] => {
|
||||
if (Array.isArray(r)) return r as Record<string, unknown>[];
|
||||
if (r && typeof r === "object" && "rows" in r) {
|
||||
return (r as { rows: Record<string, unknown>[] }).rows;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const runQueryInTxn = async ({
|
||||
db,
|
||||
query,
|
||||
}: {
|
||||
db: DB;
|
||||
query: SQL;
|
||||
}): Promise<Record<string, unknown>[]> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
await tx.execute(sql.raw(`SET LOCAL statement_timeout = ${STATEMENT_TIMEOUT_MS}`));
|
||||
if (READ_ONLY) {
|
||||
await tx.execute(sql.raw("SET LOCAL transaction_read_only = on"));
|
||||
}
|
||||
return normalizeRows(await tx.execute(query));
|
||||
});
|
||||
};
|
||||
|
||||
const measureCell = async ({
|
||||
db,
|
||||
cell,
|
||||
}: {
|
||||
db: DB;
|
||||
cell: Cell;
|
||||
}): Promise<CellResult> => {
|
||||
const samples: number[] = [];
|
||||
let rowCount = 0;
|
||||
let error: string | undefined;
|
||||
|
||||
for (let i = 0; i < REPEATS; i++) {
|
||||
const query = cell.build();
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
const result = await runQueryInTxn({ db, query });
|
||||
const elapsed = performance.now() - startedAt;
|
||||
samples.push(elapsed);
|
||||
rowCount = result.length;
|
||||
} catch (err) {
|
||||
const elapsed = performance.now() - startedAt;
|
||||
samples.push(elapsed);
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let explainPlan: string | undefined;
|
||||
if (cell.includeExplain && !error) {
|
||||
try {
|
||||
const query = cell.build();
|
||||
const explainRows = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query}`,
|
||||
});
|
||||
explainPlan = explainRows.map((row) => row["QUERY PLAN"]).join("\n");
|
||||
} catch (err) {
|
||||
explainPlan = `EXPLAIN failed: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
|
||||
const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0;
|
||||
const min = sorted[0] ?? 0;
|
||||
const max = sorted[sorted.length - 1] ?? 0;
|
||||
|
||||
return {
|
||||
name: cell.name,
|
||||
medianMs: median,
|
||||
p95Ms: p95,
|
||||
minMs: min,
|
||||
maxMs: max,
|
||||
rowCount,
|
||||
samples,
|
||||
explainPlan,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
const revenuecatCountQuery = (): SQL => sql`
|
||||
SELECT COUNT(*)::int AS total_count
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID}
|
||||
AND c.env = ${ENV}
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = c.internal_id
|
||||
AND cp.processor->>'type' = 'revenuecat'
|
||||
)
|
||||
`;
|
||||
|
||||
const resolveRevenuecatCount = async ({ db }: { db: DB }) => {
|
||||
const result = await runQueryInTxn({ db, query: revenuecatCountQuery() });
|
||||
return (result[0] as { total_count: number }).total_count;
|
||||
};
|
||||
|
||||
const resolveRevenuecatDeepCursor = async ({
|
||||
db,
|
||||
deepOffset,
|
||||
}: {
|
||||
db: DB;
|
||||
deepOffset: number;
|
||||
}): Promise<{ v: 0; t: number; id: string }> => {
|
||||
const result = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT c.created_at, c.id
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID}
|
||||
AND c.env = ${ENV}
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = c.internal_id
|
||||
AND cp.processor->>'type' = 'revenuecat'
|
||||
)
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1 OFFSET ${deepOffset}
|
||||
`,
|
||||
});
|
||||
|
||||
const row = result[0] as { created_at: number; id: string } | undefined;
|
||||
if (!row) {
|
||||
throw new Error(
|
||||
`Could not resolve revenuecat deep cursor at offset ${deepOffset}. Not enough revenuecat customers.`,
|
||||
);
|
||||
}
|
||||
return { v: 0, t: row.created_at, id: row.id };
|
||||
};
|
||||
|
||||
const buildCells = ({
|
||||
deepCursor,
|
||||
}: {
|
||||
deepCursor: { v: 0; t: number; id: string };
|
||||
}): Cell[] => {
|
||||
return [
|
||||
{
|
||||
name: `10 cursor / revcat=true / first page / limit ${BASE_LIMIT}`,
|
||||
build: () =>
|
||||
getCursorPaginatedFullCusQuery({
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
limit: BASE_LIMIT,
|
||||
processors: ["revenuecat"],
|
||||
...sharedFullCusOpts,
|
||||
}),
|
||||
includeExplain: true,
|
||||
},
|
||||
{
|
||||
name: `11 cursor / revcat=true / deep / limit ${BASE_LIMIT}`,
|
||||
build: () =>
|
||||
getCursorPaginatedFullCusQuery({
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
limit: BASE_LIMIT,
|
||||
cursor: deepCursor,
|
||||
processors: ["revenuecat"],
|
||||
...sharedFullCusOpts,
|
||||
}),
|
||||
includeExplain: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const renderResults = ({
|
||||
totalCount,
|
||||
deepOffset,
|
||||
deepCursor,
|
||||
results,
|
||||
}: {
|
||||
totalCount: number;
|
||||
deepOffset: number;
|
||||
deepCursor: { v: 0; t: number; id: string };
|
||||
results: CellResult[];
|
||||
}): string => {
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`# Pagination Benchmark — ${date} — prod / ${LABEL}`);
|
||||
lines.push("");
|
||||
lines.push("## Config");
|
||||
lines.push("");
|
||||
lines.push(`- org_id: \`${ORG_ID}\``);
|
||||
lines.push(`- env: \`${ENV}\``);
|
||||
lines.push(`- revenuecat_customers: \`${totalCount.toLocaleString()}\``);
|
||||
lines.push(
|
||||
`- deep_offset (${DEEP_OFFSET_PCT}% within revcat subset): \`${deepOffset.toLocaleString()}\``,
|
||||
);
|
||||
lines.push(
|
||||
`- deep_cursor (revcat-aware): \`{ t: ${deepCursor.t}, id: ${deepCursor.id} }\``,
|
||||
);
|
||||
lines.push(`- limit: \`${BASE_LIMIT}\``);
|
||||
lines.push(`- repeats per cell: ${REPEATS}`);
|
||||
lines.push(`- statement_timeout_ms: ${STATEMENT_TIMEOUT_MS}`);
|
||||
lines.push(`- read_only: ${READ_ONLY}`);
|
||||
lines.push(`- focus: revenuecat filter (single SQL call, cursor + EXISTS combined)`);
|
||||
lines.push(`- partial index in place: idx_customer_products_revenuecat_processor`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Results");
|
||||
lines.push("");
|
||||
lines.push("| # | Cell | Rows | median ms | p95 ms | min ms | max ms | Error |");
|
||||
lines.push("|---|------|------|-----------|--------|--------|--------|-------|");
|
||||
for (const r of results) {
|
||||
const idx = r.name.split(" ")[0];
|
||||
const desc = r.name.split(" ").slice(1).join(" ");
|
||||
const errCol = r.error ? `\`${r.error.slice(0, 80)}\`` : "";
|
||||
lines.push(
|
||||
`| ${idx} | ${desc} | ${r.rowCount} | ${r.medianMs.toFixed(2)} | ${r.p95Ms.toFixed(2)} | ${r.minMs.toFixed(2)} | ${r.maxMs.toFixed(2)} | ${errCol} |`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("## EXPLAIN ANALYZE");
|
||||
lines.push("");
|
||||
for (const r of results) {
|
||||
if (!r.explainPlan) continue;
|
||||
lines.push(`### ${r.name}`);
|
||||
lines.push("");
|
||||
lines.push("```");
|
||||
lines.push(r.explainPlan);
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n================ Pagination Benchmark — prod / ${LABEL} ================\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
try {
|
||||
console.log(chalk.cyan(`Org: ${ORG_ID} (${ENV})`));
|
||||
console.log(chalk.cyan("Resolving revenuecat customer count..."));
|
||||
const totalCount = await resolveRevenuecatCount({ db });
|
||||
const deepOffset = Math.floor((totalCount * DEEP_OFFSET_PCT) / 100);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` revenuecat_customers = ${totalCount.toLocaleString()}, deep_offset (${DEEP_OFFSET_PCT}% within revcat) = ${deepOffset.toLocaleString()}`,
|
||||
),
|
||||
);
|
||||
|
||||
console.log(
|
||||
chalk.cyan(`Resolving revcat-aware deep cursor at offset ${deepOffset}...`),
|
||||
);
|
||||
const deepCursor = await resolveRevenuecatDeepCursor({ db, deepOffset });
|
||||
console.log(
|
||||
chalk.gray(` cursor = { t: ${deepCursor.t}, id: ${deepCursor.id} }`),
|
||||
);
|
||||
console.log();
|
||||
|
||||
const cells = buildCells({ deepCursor });
|
||||
const results: CellResult[] = [];
|
||||
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const cell = cells[i];
|
||||
process.stdout.write(
|
||||
chalk.gray(`[${i + 1}/${cells.length}] ${cell.name} ... `),
|
||||
);
|
||||
const result = await measureCell({ db, cell });
|
||||
results.push(result);
|
||||
if (result.error) {
|
||||
console.log(chalk.red(`error: ${result.error.slice(0, 80)}`));
|
||||
} else {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`median=${result.medianMs.toFixed(2)}ms p95=${result.p95Ms.toFixed(2)}ms rows=${result.rowCount}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const report = renderResults({
|
||||
totalCount,
|
||||
deepOffset,
|
||||
deepCursor,
|
||||
results,
|
||||
});
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const outPath = join(RESULTS_DIR, `${date}-prod-${LABEL}.md`);
|
||||
writeFileSync(outPath, report);
|
||||
|
||||
console.log();
|
||||
console.log(chalk.green(`✅ Report written to ${outPath}`));
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n================ Benchmark Complete ================\n`,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red("\n❌ Benchmark failed:"));
|
||||
if (error instanceof Error) {
|
||||
console.error(chalk.red(` ${error.message}`));
|
||||
console.error(chalk.gray(error.stack));
|
||||
} else {
|
||||
console.error(chalk.red(` ${String(error)}`));
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,678 +0,0 @@
|
||||
import { AppEnv, CusProductStatus, type ListCustomersV2Params } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql, type SQL } from "drizzle-orm";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
import { getPaginatedFullCusQuery } from "../src/internal/customers/getFullCusQuery";
|
||||
import { RELEVANT_STATUSES } from "../src/internal/customers/cusProducts/CusProductService";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
// Runs against whatever DATABASE_URL points at. Intentionally bypasses
|
||||
// the prod-safety guard — laptop → prod is the entire point.
|
||||
|
||||
const FIRECRAWL_ORG_ID = "biu9vSF7vghBLSKW1UTDwxHBAivjnPaK";
|
||||
const RUNABLE_ORG_ID = "r7pwHiekGsqt32qGqcVku6thWFh5aHh8";
|
||||
const ENV: AppEnv = AppEnv.Live;
|
||||
|
||||
const BASE_LIMIT = 1000;
|
||||
const REPEATS = 5;
|
||||
const STATEMENT_TIMEOUT_MS = 60_000;
|
||||
const READ_ONLY = true;
|
||||
const CUS_PRODUCT_LIMIT = 15;
|
||||
const DEEP_OFFSET_PCT = 45;
|
||||
const FIRECRAWL_BASELINE_DEEP_OFFSET = 950_000;
|
||||
|
||||
const PLAN_MASSIVE = "free";
|
||||
const PLAN_MID = "hobby";
|
||||
const PLAN_RARE = "scale_monthly";
|
||||
const SEARCH_GMAIL = "@gmail";
|
||||
|
||||
const RESULTS_DIR = join(import.meta.dir, "results");
|
||||
|
||||
type DB = ReturnType<typeof initDrizzle>["db"];
|
||||
type Cursor = { v: 0; t: number; id: string };
|
||||
|
||||
type FilterOverrides = {
|
||||
inStatuses?: CusProductStatus[];
|
||||
search?: string;
|
||||
plans?: ListCustomersV2Params["plans"];
|
||||
processors?: ListCustomersV2Params["processors"];
|
||||
internalCustomerIds?: string[];
|
||||
};
|
||||
|
||||
type Scenario = {
|
||||
key: string;
|
||||
label: string;
|
||||
org: "firecrawl" | "runable";
|
||||
orgId: string;
|
||||
filter: FilterOverrides;
|
||||
withDeep: boolean;
|
||||
deepOffset?: number;
|
||||
deepCursor?: Cursor;
|
||||
};
|
||||
|
||||
type Cell = {
|
||||
scenarioKey: string;
|
||||
scenarioLabel: string;
|
||||
org: "firecrawl" | "runable";
|
||||
queryShape: "offset" | "cursor";
|
||||
depth: "page1" | "deep";
|
||||
build: () => SQL;
|
||||
};
|
||||
|
||||
type CellResult = Cell & {
|
||||
medianMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
rowCount: number;
|
||||
samples: number[];
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const sharedFullCusOpts = {
|
||||
includeInvoices: false,
|
||||
withEntities: false,
|
||||
withTrialsUsed: false,
|
||||
withSubs: true,
|
||||
cusProductLimit: CUS_PRODUCT_LIMIT,
|
||||
};
|
||||
|
||||
const normalizeRows = (r: unknown): Record<string, unknown>[] => {
|
||||
if (Array.isArray(r)) return r as Record<string, unknown>[];
|
||||
if (r && typeof r === "object" && "rows" in r) {
|
||||
return (r as { rows: Record<string, unknown>[] }).rows;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const runQueryInTxn = async ({
|
||||
db,
|
||||
query,
|
||||
}: { db: DB; query: SQL }): Promise<Record<string, unknown>[]> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
await tx.execute(
|
||||
sql.raw(`SET LOCAL statement_timeout = ${STATEMENT_TIMEOUT_MS}`),
|
||||
);
|
||||
if (READ_ONLY) {
|
||||
await tx.execute(sql.raw("SET LOCAL transaction_read_only = on"));
|
||||
}
|
||||
return normalizeRows(await tx.execute(query));
|
||||
});
|
||||
};
|
||||
|
||||
const measureCell = async ({
|
||||
db,
|
||||
cell,
|
||||
}: { db: DB; cell: Cell }): Promise<CellResult> => {
|
||||
const samples: number[] = [];
|
||||
let rowCount = 0;
|
||||
let error: string | undefined;
|
||||
|
||||
for (let i = 0; i < REPEATS; i++) {
|
||||
const query = cell.build();
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
const result = await runQueryInTxn({ db, query });
|
||||
const elapsed = performance.now() - startedAt;
|
||||
samples.push(elapsed);
|
||||
rowCount = result.length;
|
||||
} catch (err) {
|
||||
const elapsed = performance.now() - startedAt;
|
||||
samples.push(elapsed);
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
|
||||
const p95 =
|
||||
sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0;
|
||||
const min = sorted[0] ?? 0;
|
||||
const max = sorted[sorted.length - 1] ?? 0;
|
||||
|
||||
return {
|
||||
...cell,
|
||||
medianMs: median,
|
||||
p95Ms: p95,
|
||||
minMs: min,
|
||||
maxMs: max,
|
||||
rowCount,
|
||||
samples,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
// Per-filter predicate matching getCustomerListFilterSql, used by the
|
||||
// resolver helpers (count + deep cursor) so deep cells are filter-aware.
|
||||
const filterPredicate = (f: FilterOverrides): SQL => {
|
||||
const parts: SQL[] = [];
|
||||
|
||||
if (f.internalCustomerIds?.length) {
|
||||
parts.push(
|
||||
sql`AND c.internal_id IN (${sql.join(
|
||||
f.internalCustomerIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (f.plans?.length) {
|
||||
const planConditions = f.plans.map((plan) => {
|
||||
if (plan.versions?.length) {
|
||||
return sql`(p_filter.id = ${plan.id} AND p_filter.version IN (${sql.join(
|
||||
plan.versions.map((v) => sql`${v}`),
|
||||
sql`, `,
|
||||
)}))`;
|
||||
}
|
||||
return sql`p_filter.id = ${plan.id}`;
|
||||
});
|
||||
parts.push(sql`AND EXISTS (
|
||||
SELECT 1
|
||||
FROM customer_products cp_filter
|
||||
JOIN products p_filter ON cp_filter.internal_product_id = p_filter.internal_id
|
||||
WHERE cp_filter.internal_customer_id = c.internal_id
|
||||
${
|
||||
f.inStatuses?.length
|
||||
? sql`AND cp_filter.status = ANY(ARRAY[${sql.join(
|
||||
f.inStatuses.map((s) => sql`${s}`),
|
||||
sql`, `,
|
||||
)}])`
|
||||
: sql``
|
||||
}
|
||||
AND (${sql.join(planConditions, sql` OR `)})
|
||||
)`);
|
||||
}
|
||||
|
||||
const trimmedSearch = f.search?.trim();
|
||||
if (trimmedSearch) {
|
||||
const pattern = `%${trimmedSearch}%`;
|
||||
parts.push(sql`AND (
|
||||
c.id ILIKE ${pattern}
|
||||
OR c.name ILIKE ${pattern}
|
||||
OR c.email ILIKE ${pattern}
|
||||
)`);
|
||||
}
|
||||
|
||||
if (f.processors?.length) {
|
||||
const procConditions = f.processors
|
||||
.map((proc) => {
|
||||
if (proc === "stripe") return sql`(c.processor->>'id' IS NOT NULL)`;
|
||||
if (proc === "revenuecat")
|
||||
return sql`EXISTS (
|
||||
SELECT 1
|
||||
FROM customer_products cp_processor
|
||||
WHERE cp_processor.internal_customer_id = c.internal_id
|
||||
AND cp_processor.processor->>'type' = 'revenuecat'
|
||||
)`;
|
||||
if (proc === "vercel") return sql`(c.processors->>'vercel' IS NOT NULL)`;
|
||||
return null;
|
||||
})
|
||||
.filter((c): c is SQL => c !== null);
|
||||
|
||||
if (procConditions.length) {
|
||||
parts.push(sql`AND (${sql.join(procConditions, sql` OR `)})`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length ? sql.join(parts, sql` `) : sql``;
|
||||
};
|
||||
|
||||
const resolveCount = async ({
|
||||
db,
|
||||
orgId,
|
||||
filter,
|
||||
}: { db: DB; orgId: string; filter: FilterOverrides }): Promise<number> => {
|
||||
const predicate = filterPredicate(filter);
|
||||
const rows = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT COUNT(*)::int AS total_count
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${orgId}
|
||||
AND c.env = ${ENV}
|
||||
${predicate}
|
||||
`,
|
||||
});
|
||||
return (rows[0] as { total_count: number }).total_count;
|
||||
};
|
||||
|
||||
const resolveDeepCursor = async ({
|
||||
db,
|
||||
orgId,
|
||||
filter,
|
||||
deepOffset,
|
||||
}: {
|
||||
db: DB;
|
||||
orgId: string;
|
||||
filter: FilterOverrides;
|
||||
deepOffset: number;
|
||||
}): Promise<Cursor> => {
|
||||
const predicate = filterPredicate(filter);
|
||||
const rows = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT c.created_at, c.id
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${orgId}
|
||||
AND c.env = ${ENV}
|
||||
${predicate}
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1 OFFSET ${deepOffset}
|
||||
`,
|
||||
});
|
||||
const row = rows[0] as { created_at: number; id: string } | undefined;
|
||||
if (!row) {
|
||||
throw new Error(
|
||||
`No row at deep offset ${deepOffset} for filter ${JSON.stringify(filter)}`,
|
||||
);
|
||||
}
|
||||
return { v: 0, t: row.created_at, id: row.id };
|
||||
};
|
||||
|
||||
const resolveFirecrawlInternalIds = async ({
|
||||
db,
|
||||
}: { db: DB }): Promise<string[]> => {
|
||||
const rows = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT internal_id
|
||||
FROM customers
|
||||
WHERE org_id = ${FIRECRAWL_ORG_ID}
|
||||
AND env = ${ENV}
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 10
|
||||
`,
|
||||
});
|
||||
return rows.map((r) => (r as { internal_id: string }).internal_id);
|
||||
};
|
||||
|
||||
const buildScenarios = ({
|
||||
firecrawlInternalIds,
|
||||
}: { firecrawlInternalIds: string[] }): Scenario[] => {
|
||||
const fc = (
|
||||
key: string,
|
||||
label: string,
|
||||
filter: FilterOverrides,
|
||||
withDeep: boolean,
|
||||
): Scenario => ({
|
||||
key,
|
||||
label,
|
||||
org: "firecrawl",
|
||||
orgId: FIRECRAWL_ORG_ID,
|
||||
filter,
|
||||
withDeep,
|
||||
});
|
||||
|
||||
return [
|
||||
fc("01-baseline", "none (baseline)", {}, true),
|
||||
fc("02-search-gmail", `search '${SEARCH_GMAIL}'`, { search: SEARCH_GMAIL }, true),
|
||||
fc(
|
||||
"03-status-active",
|
||||
"inStatuses=['active']",
|
||||
{ inStatuses: [CusProductStatus.Active] },
|
||||
true,
|
||||
),
|
||||
fc("04-plan-massive", `plans=['${PLAN_MASSIVE}']`, { plans: [{ id: PLAN_MASSIVE }] }, true),
|
||||
fc("05-plan-mid", `plans=['${PLAN_MID}']`, { plans: [{ id: PLAN_MID }] }, true),
|
||||
fc("06-plan-rare", `plans=['${PLAN_RARE}']`, { plans: [{ id: PLAN_RARE }] }, false),
|
||||
fc("07-processor-stripe", "processors=['stripe']", { processors: ["stripe"] }, true),
|
||||
fc(
|
||||
"08-internal-ids",
|
||||
"internalCustomerIds=[10]",
|
||||
{ internalCustomerIds: firecrawlInternalIds },
|
||||
false,
|
||||
),
|
||||
{
|
||||
key: "09-processor-revenuecat",
|
||||
label: "processors=['revenuecat']",
|
||||
org: "runable",
|
||||
orgId: RUNABLE_ORG_ID,
|
||||
filter: { processors: ["revenuecat"] },
|
||||
withDeep: false,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const buildCellsForScenario = (s: Scenario): Cell[] => {
|
||||
const inStatuses = s.filter.inStatuses ?? RELEVANT_STATUSES;
|
||||
const filterArgs = {
|
||||
inStatuses,
|
||||
search: s.filter.search,
|
||||
plans: s.filter.plans,
|
||||
processors: s.filter.processors,
|
||||
internalCustomerIds: s.filter.internalCustomerIds,
|
||||
};
|
||||
|
||||
const cells: Cell[] = [
|
||||
{
|
||||
scenarioKey: s.key,
|
||||
scenarioLabel: s.label,
|
||||
org: s.org,
|
||||
queryShape: "offset",
|
||||
depth: "page1",
|
||||
build: () =>
|
||||
getPaginatedFullCusQuery({
|
||||
orgId: s.orgId,
|
||||
env: ENV,
|
||||
limit: BASE_LIMIT,
|
||||
offset: 0,
|
||||
...sharedFullCusOpts,
|
||||
...filterArgs,
|
||||
}),
|
||||
},
|
||||
{
|
||||
scenarioKey: s.key,
|
||||
scenarioLabel: s.label,
|
||||
org: s.org,
|
||||
queryShape: "cursor",
|
||||
depth: "page1",
|
||||
build: () =>
|
||||
getCursorPaginatedFullCusQuery({
|
||||
orgId: s.orgId,
|
||||
env: ENV,
|
||||
limit: BASE_LIMIT,
|
||||
...sharedFullCusOpts,
|
||||
...filterArgs,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (s.withDeep && s.deepOffset !== undefined && s.deepCursor) {
|
||||
const deepOffset = s.deepOffset;
|
||||
const deepCursor = s.deepCursor;
|
||||
cells.push(
|
||||
{
|
||||
scenarioKey: s.key,
|
||||
scenarioLabel: s.label,
|
||||
org: s.org,
|
||||
queryShape: "offset",
|
||||
depth: "deep",
|
||||
build: () =>
|
||||
getPaginatedFullCusQuery({
|
||||
orgId: s.orgId,
|
||||
env: ENV,
|
||||
limit: BASE_LIMIT,
|
||||
offset: deepOffset,
|
||||
...sharedFullCusOpts,
|
||||
...filterArgs,
|
||||
}),
|
||||
},
|
||||
{
|
||||
scenarioKey: s.key,
|
||||
scenarioLabel: s.label,
|
||||
org: s.org,
|
||||
queryShape: "cursor",
|
||||
depth: "deep",
|
||||
build: () =>
|
||||
getCursorPaginatedFullCusQuery({
|
||||
orgId: s.orgId,
|
||||
env: ENV,
|
||||
limit: BASE_LIMIT,
|
||||
cursor: deepCursor,
|
||||
...sharedFullCusOpts,
|
||||
...filterArgs,
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return cells;
|
||||
};
|
||||
|
||||
const pairResults = (
|
||||
results: CellResult[],
|
||||
): {
|
||||
scenarioKey: string;
|
||||
scenarioLabel: string;
|
||||
org: string;
|
||||
depth: "page1" | "deep";
|
||||
offset?: CellResult;
|
||||
cursor?: CellResult;
|
||||
}[] => {
|
||||
const groups = new Map<
|
||||
string,
|
||||
{
|
||||
scenarioKey: string;
|
||||
scenarioLabel: string;
|
||||
org: string;
|
||||
depth: "page1" | "deep";
|
||||
offset?: CellResult;
|
||||
cursor?: CellResult;
|
||||
}
|
||||
>();
|
||||
for (const r of results) {
|
||||
const key = `${r.scenarioKey}|${r.depth}`;
|
||||
const group = groups.get(key) ?? {
|
||||
scenarioKey: r.scenarioKey,
|
||||
scenarioLabel: r.scenarioLabel,
|
||||
org: r.org,
|
||||
depth: r.depth,
|
||||
};
|
||||
if (r.queryShape === "offset") group.offset = r;
|
||||
else group.cursor = r;
|
||||
groups.set(key, group);
|
||||
}
|
||||
return Array.from(groups.values());
|
||||
};
|
||||
|
||||
const fmt = (n: number | undefined): string =>
|
||||
n === undefined ? "—" : n.toFixed(0);
|
||||
|
||||
const renderReport = ({
|
||||
scenarios,
|
||||
pairs,
|
||||
totalWallMs,
|
||||
dbHost,
|
||||
}: {
|
||||
scenarios: Scenario[];
|
||||
pairs: ReturnType<typeof pairResults>;
|
||||
totalWallMs: number;
|
||||
dbHost: string;
|
||||
}): string => {
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`# Pagination Benchmark — offset vs cursor — ${date}`);
|
||||
lines.push("");
|
||||
lines.push("## Config");
|
||||
lines.push("");
|
||||
lines.push(`- db host: \`${dbHost}\``);
|
||||
lines.push(`- env: \`${ENV}\``);
|
||||
lines.push(`- limit: ${BASE_LIMIT}`);
|
||||
lines.push(`- repeats per cell: ${REPEATS}`);
|
||||
lines.push(`- statement_timeout_ms: ${STATEMENT_TIMEOUT_MS}`);
|
||||
lines.push(`- read_only: ${READ_ONLY}`);
|
||||
lines.push(`- shared opts: withSubs=true, includeInvoices=false, withEntities=false, withTrialsUsed=false, cusProductLimit=${CUS_PRODUCT_LIMIT}`);
|
||||
lines.push(`- timings are wall-clock from this laptop around drizzle execute (includes network round-trip + result transfer + deserialize)`);
|
||||
lines.push(`- total benchmark wall time: ${(totalWallMs / 1000).toFixed(1)}s`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Scenarios resolved");
|
||||
lines.push("");
|
||||
lines.push("| # | Org | Filter | Filtered count | Deep offset | Deep cursor |");
|
||||
lines.push("|---|-----|--------|----------------|-------------|-------------|");
|
||||
for (const s of scenarios) {
|
||||
const count = (s as Scenario & { resolvedCount?: number }).resolvedCount;
|
||||
lines.push(
|
||||
`| ${s.key} | ${s.org} | \`${s.label}\` | ${count?.toLocaleString() ?? "—"} | ${s.deepOffset?.toLocaleString() ?? "—"} | ${s.deepCursor ? `\`{ t: ${s.deepCursor.t}, id: ${s.deepCursor.id.slice(0, 8)}… }\`` : "—"} |`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Results");
|
||||
lines.push("");
|
||||
lines.push(
|
||||
"| # | Org | Filter | Depth | offset median ms | cursor median ms | Δ ms | offset p95 | cursor p95 | offset rows | cursor rows | offset error | cursor error |",
|
||||
);
|
||||
lines.push(
|
||||
"|---|-----|--------|-------|------------------|------------------|------|------------|------------|-------------|-------------|--------------|--------------|",
|
||||
);
|
||||
for (const p of pairs) {
|
||||
const off = p.offset;
|
||||
const cur = p.cursor;
|
||||
const delta =
|
||||
off && cur && !off.error && !cur.error
|
||||
? cur.medianMs - off.medianMs
|
||||
: undefined;
|
||||
const deltaStr =
|
||||
delta === undefined
|
||||
? "—"
|
||||
: `${delta >= 0 ? "+" : ""}${delta.toFixed(0)}`;
|
||||
lines.push(
|
||||
`| ${p.scenarioKey} | ${p.org} | \`${p.scenarioLabel}\` | ${p.depth} | ${fmt(off?.medianMs)} | ${fmt(cur?.medianMs)} | ${deltaStr} | ${fmt(off?.p95Ms)} | ${fmt(cur?.p95Ms)} | ${off?.rowCount ?? "—"} | ${cur?.rowCount ?? "—"} | ${off?.error ? `\`${off.error.slice(0, 50)}\`` : ""} | ${cur?.error ? `\`${cur.error.slice(0, 50)}\`` : ""} |`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Raw samples (per cell)");
|
||||
lines.push("");
|
||||
for (const p of pairs) {
|
||||
lines.push(`### ${p.scenarioKey} / ${p.depth}`);
|
||||
if (p.offset) {
|
||||
lines.push(
|
||||
`- offset: [${p.offset.samples.map((s) => s.toFixed(0)).join(", ")}]ms${p.offset.error ? ` (error: ${p.offset.error})` : ""}`,
|
||||
);
|
||||
}
|
||||
if (p.cursor) {
|
||||
lines.push(
|
||||
`- cursor: [${p.cursor.samples.map((s) => s.toFixed(0)).join(", ")}]ms${p.cursor.error ? ` (error: ${p.cursor.error})` : ""}`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
const redactHost = (url: string | undefined): string => {
|
||||
if (!url) return "<unset>";
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return `${u.hostname}:${u.port || "5432"}`;
|
||||
} catch {
|
||||
return "<unparseable>";
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const startedAt = performance.now();
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Pagination Benchmark — offset vs cursor (prod DB) ================\n",
|
||||
),
|
||||
);
|
||||
|
||||
const dbHost = redactHost(process.env.DATABASE_URL);
|
||||
console.log(chalk.cyan(`db host: ${dbHost}`));
|
||||
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
try {
|
||||
console.log(chalk.cyan("Resolving Firecrawl internal_ids for IN-list cell..."));
|
||||
const firecrawlInternalIds = await resolveFirecrawlInternalIds({ db });
|
||||
console.log(chalk.gray(` got ${firecrawlInternalIds.length} ids`));
|
||||
|
||||
const scenarios = buildScenarios({ firecrawlInternalIds });
|
||||
|
||||
// Resolve counts and deep cursors for each scenario before running cells.
|
||||
for (const s of scenarios) {
|
||||
process.stdout.write(chalk.gray(`Resolving count for ${s.key} (${s.label})... `));
|
||||
const count = await resolveCount({ db, orgId: s.orgId, filter: s.filter });
|
||||
(s as Scenario & { resolvedCount?: number }).resolvedCount = count;
|
||||
console.log(chalk.gray(`${count.toLocaleString()} customers`));
|
||||
|
||||
if (s.withDeep) {
|
||||
const deepOffset =
|
||||
s.key === "01-baseline"
|
||||
? FIRECRAWL_BASELINE_DEEP_OFFSET
|
||||
: Math.floor((count * DEEP_OFFSET_PCT) / 100);
|
||||
s.deepOffset = deepOffset;
|
||||
process.stdout.write(
|
||||
chalk.gray(` resolving deep cursor at offset ${deepOffset}... `),
|
||||
);
|
||||
try {
|
||||
s.deepCursor = await resolveDeepCursor({
|
||||
db,
|
||||
orgId: s.orgId,
|
||||
filter: s.filter,
|
||||
deepOffset,
|
||||
});
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`{ t: ${s.deepCursor.t}, id: ${s.deepCursor.id.slice(0, 8)}… }`,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
),
|
||||
);
|
||||
s.withDeep = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cells = scenarios.flatMap(buildCellsForScenario);
|
||||
console.log();
|
||||
console.log(chalk.cyan(`Running ${cells.length} cells × ${REPEATS} repeats...`));
|
||||
console.log();
|
||||
|
||||
const results: CellResult[] = [];
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const cell = cells[i];
|
||||
process.stdout.write(
|
||||
chalk.gray(
|
||||
`[${(i + 1).toString().padStart(2)}/${cells.length}] ${cell.scenarioKey} ${cell.queryShape} ${cell.depth} ... `,
|
||||
),
|
||||
);
|
||||
const result = await measureCell({ db, cell });
|
||||
results.push(result);
|
||||
if (result.error) {
|
||||
console.log(chalk.red(`error: ${result.error.slice(0, 80)}`));
|
||||
} else {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`median=${result.medianMs.toFixed(0)}ms p95=${result.p95Ms.toFixed(0)}ms rows=${result.rowCount}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const pairs = pairResults(results);
|
||||
const totalWallMs = performance.now() - startedAt;
|
||||
const report = renderReport({ scenarios, pairs, totalWallMs, dbHost });
|
||||
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const outPath = join(RESULTS_DIR, `${date}-offset-vs-cursor.md`);
|
||||
writeFileSync(outPath, report);
|
||||
|
||||
console.log();
|
||||
console.log(chalk.green(`✅ Report written to ${outPath}`));
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n================ Benchmark Complete (${(totalWallMs / 1000).toFixed(1)}s) ================\n`,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red("\n❌ Benchmark failed:"));
|
||||
if (error instanceof Error) {
|
||||
console.error(chalk.red(` ${error.message}`));
|
||||
console.error(chalk.gray(error.stack));
|
||||
} else {
|
||||
console.error(chalk.red(` ${String(error)}`));
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,304 +0,0 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql, type 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 { getOptimizedFullCusQuery } from "./optimizedFullCusQuery";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
const ENV: AppEnv = AppEnv.Sandbox;
|
||||
const REPEATS = 5;
|
||||
const STATEMENT_TIMEOUT_MS = 60_000;
|
||||
const DEEP_OFFSET_PCT = parseInt(process.env.OFFSET_PCT ?? "60", 10);
|
||||
const LIMIT = parseInt(process.env.LIMIT ?? "1000", 10);
|
||||
const CUS_PRODUCT_LIMIT = 15;
|
||||
const SHOW_EXPLAIN = process.env.EXPLAIN !== "false";
|
||||
const SKIP_CURRENT = process.env.SKIP_CURRENT === "true";
|
||||
const SKIP_OPTIMIZED = process.env.SKIP_OPTIMIZED === "true";
|
||||
const DASHBOARD_MODE = process.env.DASHBOARD === "true";
|
||||
|
||||
type DB = ReturnType<typeof initDrizzle>["db"];
|
||||
|
||||
const normalizeRows = (r: unknown): Record<string, unknown>[] => {
|
||||
if (Array.isArray(r)) return r as Record<string, unknown>[];
|
||||
if (r && typeof r === "object" && "rows" in r) {
|
||||
return (r as { rows: Record<string, unknown>[] }).rows;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const runInTxn = async ({ db, query }: { db: DB; query: SQL }) => {
|
||||
return db.transaction(async (tx) => {
|
||||
await tx.execute(
|
||||
sql.raw(`SET LOCAL statement_timeout = ${STATEMENT_TIMEOUT_MS}`),
|
||||
);
|
||||
await tx.execute(sql.raw("SET LOCAL transaction_read_only = on"));
|
||||
return normalizeRows(await tx.execute(query));
|
||||
});
|
||||
};
|
||||
|
||||
const resolveTotalCount = async ({ db }: { db: DB }) => {
|
||||
const result = await runInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT COUNT(*)::int AS total_count
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID} AND c.env = ${ENV}
|
||||
`,
|
||||
});
|
||||
return (result[0] as { total_count: number }).total_count;
|
||||
};
|
||||
|
||||
const resolveDeepCursor = async ({
|
||||
db,
|
||||
offset,
|
||||
}: {
|
||||
db: DB;
|
||||
offset: number;
|
||||
}) => {
|
||||
const result = await runInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT c.created_at, c.id
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID} AND c.env = ${ENV}
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1 OFFSET ${offset}
|
||||
`,
|
||||
});
|
||||
const row = result[0] as { created_at: number; id: string } | undefined;
|
||||
if (!row) throw new Error(`No customer at offset ${offset}`);
|
||||
return { v: 0 as const, t: row.created_at, id: row.id };
|
||||
};
|
||||
|
||||
const buildQuery = (
|
||||
cursor?: { v: 0; t: number; id: string },
|
||||
variant: "current" | "optimized" = "current",
|
||||
internalCustomerIds?: string[],
|
||||
): SQL => {
|
||||
const builder =
|
||||
variant === "optimized"
|
||||
? getOptimizedFullCusQuery
|
||||
: getCursorPaginatedFullCusQuery;
|
||||
return builder({
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
withSubs: true,
|
||||
limit: internalCustomerIds?.length ?? LIMIT,
|
||||
cursor,
|
||||
internalCustomerIds,
|
||||
cusProductLimit: CUS_PRODUCT_LIMIT,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveInternalIds = async ({
|
||||
db,
|
||||
offset,
|
||||
limit,
|
||||
}: {
|
||||
db: DB;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}): Promise<string[]> => {
|
||||
const result = await runInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT c.internal_id
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID} AND c.env = ${ENV}
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`,
|
||||
});
|
||||
return (result as unknown as { internal_id: string }[]).map(
|
||||
(r) => r.internal_id,
|
||||
);
|
||||
};
|
||||
|
||||
const measureRun = async ({
|
||||
db,
|
||||
cursor,
|
||||
label,
|
||||
variant = "current",
|
||||
internalCustomerIds,
|
||||
}: {
|
||||
db: DB;
|
||||
cursor?: { v: 0; t: number; id: string };
|
||||
label: string;
|
||||
variant?: "current" | "optimized";
|
||||
internalCustomerIds?: string[];
|
||||
}) => {
|
||||
const samples: number[] = [];
|
||||
for (let i = 0; i < REPEATS; i++) {
|
||||
const t0 = performance.now();
|
||||
await runInTxn({ db, query: buildQuery(cursor, variant, internalCustomerIds) });
|
||||
const ms = performance.now() - t0;
|
||||
samples.push(ms);
|
||||
process.stdout.write(chalk.gray(` run ${i + 1}/${REPEATS}: ${ms.toFixed(0)}ms\n`));
|
||||
}
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
|
||||
const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0;
|
||||
const min = sorted[0] ?? 0;
|
||||
const max = sorted[sorted.length - 1] ?? 0;
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` ${label}: median=${median.toFixed(0)}ms p95=${p95.toFixed(0)}ms min=${min.toFixed(0)}ms max=${max.toFixed(0)}ms`,
|
||||
),
|
||||
);
|
||||
return { median, p95, min, max, samples };
|
||||
};
|
||||
|
||||
const runExplain = async ({
|
||||
db,
|
||||
cursor,
|
||||
variant = "current",
|
||||
internalCustomerIds,
|
||||
}: {
|
||||
db: DB;
|
||||
cursor?: { v: 0; t: number; id: string };
|
||||
variant?: "current" | "optimized";
|
||||
internalCustomerIds?: string[];
|
||||
}) => {
|
||||
const explain = await runInTxn({
|
||||
db,
|
||||
query: sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${buildQuery(cursor, variant, internalCustomerIds)}`,
|
||||
});
|
||||
return explain.map((r) => r["QUERY PLAN"]).join("\n");
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n================ SQL Bench — unit-test-org ================\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
try {
|
||||
console.log(chalk.cyan(`Org: ${ORG_ID} Env: ${ENV} Limit: ${LIMIT}`));
|
||||
console.log(chalk.cyan("Resolving total customer count..."));
|
||||
const total = await resolveTotalCount({ db });
|
||||
const deepOffset = Math.floor((total * DEEP_OFFSET_PCT) / 100);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` total=${total.toLocaleString()} deep_offset(${DEEP_OFFSET_PCT}%)=${deepOffset.toLocaleString()}`,
|
||||
),
|
||||
);
|
||||
|
||||
console.log(chalk.cyan(`Resolving deep cursor at offset ${deepOffset}...`));
|
||||
const deepCursor = await resolveDeepCursor({ db, offset: deepOffset });
|
||||
console.log(
|
||||
chalk.gray(` cursor = { t: ${deepCursor.t}, id: ${deepCursor.id} }\n`),
|
||||
);
|
||||
|
||||
let curFirst: { median: number } | null = null;
|
||||
let curDeep: { median: number } | null = null;
|
||||
let optFirst: { median: number } | null = null;
|
||||
let optDeep: { median: number } | null = null;
|
||||
|
||||
let dashIds: string[] | undefined;
|
||||
if (DASHBOARD_MODE) {
|
||||
const offset = Math.floor((total * DEEP_OFFSET_PCT) / 100);
|
||||
console.log(
|
||||
chalk.cyan(`Dashboard mode: resolving ${LIMIT} internal_ids at offset ${offset}...`),
|
||||
);
|
||||
dashIds = await resolveInternalIds({ db, offset, limit: LIMIT });
|
||||
console.log(chalk.gray(` got ${dashIds.length} ids\n`));
|
||||
}
|
||||
|
||||
if (!SKIP_CURRENT) {
|
||||
console.log(chalk.bold("→ CURRENT first page:"));
|
||||
curFirst = await measureRun({ db, label: "current first" });
|
||||
console.log(chalk.bold(`→ CURRENT deep page${DASHBOARD_MODE ? " (internalCustomerIds)" : ""}:`));
|
||||
curDeep = await measureRun({
|
||||
db,
|
||||
cursor: DASHBOARD_MODE ? undefined : deepCursor,
|
||||
internalCustomerIds: DASHBOARD_MODE ? dashIds : undefined,
|
||||
label: "current deep",
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.gray("(SKIP_CURRENT=true — skipping current variant)"));
|
||||
}
|
||||
|
||||
if (!SKIP_OPTIMIZED) {
|
||||
console.log();
|
||||
console.log(chalk.bold("→ OPTIMIZED first page:"));
|
||||
optFirst = await measureRun({
|
||||
db,
|
||||
label: "optim first",
|
||||
variant: "optimized",
|
||||
});
|
||||
console.log(chalk.bold(`→ OPTIMIZED deep page${DASHBOARD_MODE ? " (internalCustomerIds)" : ""}:`));
|
||||
optDeep = await measureRun({
|
||||
db,
|
||||
cursor: DASHBOARD_MODE ? undefined : deepCursor,
|
||||
internalCustomerIds: DASHBOARD_MODE ? dashIds : undefined,
|
||||
label: "optim deep",
|
||||
variant: "optimized",
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.gray("(SKIP_OPTIMIZED=true — skipping optimized variant)"));
|
||||
}
|
||||
|
||||
if (SHOW_EXPLAIN) {
|
||||
if (!SKIP_CURRENT) {
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.magenta("================ EXPLAIN: CURRENT deep ================"),
|
||||
);
|
||||
console.log(await runExplain({ db, cursor: deepCursor }));
|
||||
}
|
||||
if (!SKIP_OPTIMIZED) {
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.magenta(
|
||||
"================ EXPLAIN: OPTIMIZED deep ================",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
await runExplain({ db, cursor: deepCursor, variant: "optimized" }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.magentaBright("================ Summary ================"));
|
||||
const fmtDelta = (oldMs: number, newMs: number) => {
|
||||
const delta = ((newMs - oldMs) / oldMs) * 100;
|
||||
const arrow = delta < 0 ? "↓" : "↑";
|
||||
return chalk[delta < 0 ? "green" : "red"](
|
||||
`${arrow}${Math.abs(delta).toFixed(0)}%`,
|
||||
);
|
||||
};
|
||||
if (curFirst && optFirst) {
|
||||
console.log(
|
||||
` first page current=${curFirst.median.toFixed(0)}ms optimized=${optFirst.median.toFixed(0)}ms ${fmtDelta(curFirst.median, optFirst.median)}`,
|
||||
);
|
||||
console.log(
|
||||
` deep page current=${curDeep!.median.toFixed(0)}ms optimized=${optDeep!.median.toFixed(0)}ms ${fmtDelta(curDeep!.median, optDeep!.median)}`,
|
||||
);
|
||||
} else if (curFirst) {
|
||||
console.log(` first page current=${curFirst.median.toFixed(0)}ms`);
|
||||
console.log(` deep page current=${curDeep!.median.toFixed(0)}ms`);
|
||||
} else if (optFirst) {
|
||||
console.log(` first page optimized=${optFirst.median.toFixed(0)}ms`);
|
||||
console.log(` deep page optimized=${optDeep!.median.toFixed(0)}ms`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`\n❌ ${err instanceof Error ? err.message : err}`));
|
||||
if (err instanceof Error && err.stack) console.error(chalk.gray(err.stack));
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,130 +0,0 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { CusBatchService } from "../src/internal/customers/CusBatchService";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
const ENV = AppEnv.Sandbox;
|
||||
const PAGE_SIZE = parseInt(process.env.PAGE_SIZE ?? "100", 10);
|
||||
const MAX_PAGES = parseInt(process.env.MAX_PAGES ?? "50", 10);
|
||||
|
||||
const minimalCtx = () => {
|
||||
const { db, client } = initDrizzle();
|
||||
return {
|
||||
db,
|
||||
client,
|
||||
org: { id: ORG_ID, slug: "unit-test-org" },
|
||||
env: ENV,
|
||||
logger: {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
warn: () => {},
|
||||
debug: () => {},
|
||||
trace: () => {},
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Dashboard Cursor No-Skip Sweep ================\n",
|
||||
),
|
||||
);
|
||||
|
||||
const ctx = minimalCtx();
|
||||
|
||||
try {
|
||||
const seen = new Set<string>();
|
||||
const duplicates: string[] = [];
|
||||
const order: string[] = [];
|
||||
|
||||
let cursor: { t: number; id: string } | null = null;
|
||||
let pageNum = 0;
|
||||
while (pageNum < MAX_PAGES) {
|
||||
const res: { fullCustomers: any[]; next_cursor: string | null } =
|
||||
await CusBatchService.getDashboardCursorPage({
|
||||
ctx,
|
||||
search: "",
|
||||
filters: undefined,
|
||||
cursor,
|
||||
limit: PAGE_SIZE,
|
||||
});
|
||||
pageNum++;
|
||||
|
||||
for (const c of res.fullCustomers) {
|
||||
if (seen.has(c.internal_id)) duplicates.push(c.internal_id);
|
||||
seen.add(c.internal_id);
|
||||
order.push(c.internal_id);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` page ${pageNum.toString().padStart(3)} rows=${res.fullCustomers.length.toString().padStart(4)} total=${seen.size.toString().padStart(6)} next=${res.next_cursor ? "yes" : "no"}`,
|
||||
),
|
||||
);
|
||||
|
||||
if (!res.next_cursor) break;
|
||||
const decoded = JSON.parse(
|
||||
Buffer.from(res.next_cursor, "base64").toString("utf8"),
|
||||
);
|
||||
cursor = { t: decoded.t, id: decoded.id };
|
||||
}
|
||||
|
||||
const expected = (
|
||||
(await ctx.db.execute(
|
||||
sql`
|
||||
SELECT c.internal_id
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID} AND c.env = ${ENV}
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT ${pageNum * PAGE_SIZE}
|
||||
`,
|
||||
)) as unknown as { internal_id: string }[]
|
||||
).map((r) => r.internal_id);
|
||||
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` Walked ${pageNum} pages, saw ${seen.size} unique customers (expected first ${expected.length}).`,
|
||||
),
|
||||
);
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
console.log(
|
||||
chalk.red(` ❌ ${duplicates.length} duplicates returned across pages`),
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green(` ✓ no duplicates`));
|
||||
}
|
||||
|
||||
const missing: string[] = [];
|
||||
for (const id of expected) if (!seen.has(id)) missing.push(id);
|
||||
if (missing.length > 0) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
` ❌ ${missing.length} customers skipped! First 5: ${missing.slice(0, 5).join(", ")}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green(` ✓ no customers skipped`));
|
||||
}
|
||||
|
||||
const sameOrder = order.every((id, i) => id === expected[i]);
|
||||
console.log(
|
||||
sameOrder
|
||||
? chalk.green(` ✓ pagination order matches direct query`)
|
||||
: chalk.yellow(` ⚠ pagination order differs from direct query`),
|
||||
);
|
||||
|
||||
process.exit(missing.length === 0 && duplicates.length === 0 ? 0 : 1);
|
||||
} finally {
|
||||
await ctx.client.end();
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,463 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
import { sql, type SQL } from "drizzle-orm";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "biu9vSF7vghBLSKW1UTDwxHBAivjnPaK";
|
||||
const ENV = "live";
|
||||
const LABEL = "firecrawl-deep";
|
||||
const LIMIT = 1000;
|
||||
const FETCH_LIMIT = LIMIT + 1;
|
||||
const SQL_REPEATS = 1;
|
||||
const REASSEMBLY_REPEATS = 20;
|
||||
const STATEMENT_TIMEOUT_MS = 30_000;
|
||||
const DEEP_CURSOR = {
|
||||
t: 1774237983361,
|
||||
id: "772c9569-fc97-4c30-9fd8-c8a585b66755",
|
||||
};
|
||||
|
||||
const RESULTS_DIR = join(import.meta.dir, "results");
|
||||
|
||||
type DB = ReturnType<typeof initDrizzle>["db"];
|
||||
|
||||
const buildVariant07Sql = (): SQL => sql`
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT
|
||||
c.internal_id,
|
||||
c.id,
|
||||
c.created_at,
|
||||
row_to_json(c) AS row_json
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID}
|
||||
AND c.env = ${ENV}
|
||||
AND (c.created_at, c.id) < (${DEEP_CURSOR.t}, ${DEEP_CURSOR.id})
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT ${FETCH_LIMIT}
|
||||
),
|
||||
cps_flat AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.id,
|
||||
cp.internal_customer_id,
|
||||
cp.internal_product_id,
|
||||
cp.free_trial_id,
|
||||
cp.subscription_ids,
|
||||
(row_to_json(cp)::jsonb || jsonb_build_object('product', row_to_json(prod)))::json AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT cp.*
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
),
|
||||
ces_combined AS MATERIALIZED (
|
||||
SELECT 'bound'::text AS kind, ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cps_flat
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cps_flat.id
|
||||
) ce ON true
|
||||
UNION ALL
|
||||
SELECT 'loose'::text AS kind, ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
arrays AS MATERIALIZED (
|
||||
SELECT
|
||||
(SELECT array_agg(id) FROM ces_combined) AS all_ce_ids,
|
||||
(SELECT array_agg(DISTINCT entitlement_id) FROM ces_combined) AS distinct_entitlement_ids,
|
||||
(SELECT array_agg(DISTINCT free_trial_id) FILTER (WHERE free_trial_id IS NOT NULL) FROM cps_flat) AS free_trial_ids
|
||||
)
|
||||
SELECT
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cr) AS customers,
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cps_flat) AS customer_products,
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM ces_combined WHERE kind = 'bound') AS customer_entitlements,
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM ces_combined WHERE kind = 'loose') AS extra_customer_entitlements,
|
||||
(SELECT COALESCE(json_agg(row_to_json(cpr)::jsonb || jsonb_build_object('price', row_to_json(p))), '[]'::json)
|
||||
FROM cps_flat cps
|
||||
JOIN LATERAL (SELECT cpr.* FROM customer_prices cpr WHERE cpr.customer_product_id = cps.id) cpr ON true
|
||||
LEFT JOIN LATERAL (SELECT p.* FROM prices p WHERE p.id = cpr.price_id) p ON true) AS customer_prices,
|
||||
(SELECT COALESCE(json_agg(row_to_json(e)::jsonb || jsonb_build_object('feature', row_to_json(f))), '[]'::json)
|
||||
FROM unnest((SELECT distinct_entitlement_ids FROM arrays)) AS u(entitlement_id)
|
||||
JOIN LATERAL (SELECT e.* FROM entitlements e WHERE e.id = u.entitlement_id) e ON true
|
||||
JOIN LATERAL (SELECT f.* FROM features f WHERE f.internal_id = e.internal_feature_id) f ON true) AS entitlements,
|
||||
(SELECT COALESCE(json_agg(row_to_json(ro)), '[]'::json)
|
||||
FROM unnest((SELECT all_ce_ids FROM arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (SELECT ro.* FROM rollovers ro WHERE ro.cus_ent_id = u.ce_id AND (ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000)) ro ON true) AS rollovers,
|
||||
(SELECT COALESCE(json_agg(row_to_json(r)), '[]'::json)
|
||||
FROM unnest((SELECT all_ce_ids FROM arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (SELECT r.* FROM replaceables r WHERE r.cus_ent_id = u.ce_id) r ON true) AS replaceables,
|
||||
(SELECT COALESCE(json_agg(row_to_json(ft)), '[]'::json)
|
||||
FROM unnest((SELECT free_trial_ids FROM arrays)) AS u(ft_id)
|
||||
JOIN LATERAL (SELECT ft.* FROM free_trials ft WHERE ft.id = u.ft_id) ft ON true) AS free_trials,
|
||||
(SELECT COALESCE(json_agg(row_to_json(s)), '[]'::json)
|
||||
FROM (
|
||||
SELECT DISTINCT s.*
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN LATERAL (SELECT s.* FROM subscriptions s WHERE s.stripe_id = sub_id_t.sub_id) s ON true
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
) s) AS subscriptions
|
||||
`;
|
||||
|
||||
type V07Row = {
|
||||
customers: any[];
|
||||
customer_products: any[];
|
||||
customer_entitlements: any[];
|
||||
extra_customer_entitlements: any[];
|
||||
customer_prices: any[];
|
||||
entitlements: any[];
|
||||
rollovers: any[];
|
||||
replaceables: any[];
|
||||
free_trials: any[];
|
||||
subscriptions: any[];
|
||||
};
|
||||
|
||||
const reassemble = (flat: V07Row) => {
|
||||
const {
|
||||
customers,
|
||||
customer_products,
|
||||
customer_entitlements,
|
||||
extra_customer_entitlements,
|
||||
customer_prices,
|
||||
entitlements,
|
||||
rollovers,
|
||||
replaceables,
|
||||
free_trials,
|
||||
subscriptions,
|
||||
} = flat;
|
||||
|
||||
const entById = new Map<string, any>();
|
||||
for (const e of entitlements) entById.set(e.id, e);
|
||||
|
||||
const ftById = new Map<string, any>();
|
||||
for (const ft of free_trials) ftById.set(ft.id, ft);
|
||||
|
||||
const subByStripeId = new Map<string, any>();
|
||||
for (const s of subscriptions) subByStripeId.set(s.stripe_id, s);
|
||||
|
||||
const rolloversByCeId = new Map<string, any[]>();
|
||||
for (const ro of rollovers) {
|
||||
const list = rolloversByCeId.get(ro.cus_ent_id);
|
||||
if (list) list.push(ro);
|
||||
else rolloversByCeId.set(ro.cus_ent_id, [ro]);
|
||||
}
|
||||
|
||||
const replaceablesByCeId = new Map<string, any[]>();
|
||||
for (const r of replaceables) {
|
||||
const list = replaceablesByCeId.get(r.cus_ent_id);
|
||||
if (list) list.push(r);
|
||||
else replaceablesByCeId.set(r.cus_ent_id, [r]);
|
||||
}
|
||||
|
||||
const cpricesByCpId = new Map<string, any[]>();
|
||||
for (const cpr of customer_prices) {
|
||||
const list = cpricesByCpId.get(cpr.customer_product_id);
|
||||
if (list) list.push(cpr);
|
||||
else cpricesByCpId.set(cpr.customer_product_id, [cpr]);
|
||||
}
|
||||
|
||||
const hydrateCe = (ce: any) => ({
|
||||
...ce,
|
||||
entitlement: entById.get(ce.entitlement_id) ?? null,
|
||||
rollovers: rolloversByCeId.get(ce.id) ?? [],
|
||||
replaceables: replaceablesByCeId.get(ce.id) ?? [],
|
||||
});
|
||||
|
||||
const cesByCpId = new Map<string, any[]>();
|
||||
for (const ce of customer_entitlements) {
|
||||
const hydrated = hydrateCe(ce);
|
||||
const list = cesByCpId.get(ce.customer_product_id);
|
||||
if (list) list.push(hydrated);
|
||||
else cesByCpId.set(ce.customer_product_id, [hydrated]);
|
||||
}
|
||||
|
||||
const looseCesByCusId = new Map<string, any[]>();
|
||||
for (const ce of extra_customer_entitlements) {
|
||||
const hydrated = hydrateCe(ce);
|
||||
const list = looseCesByCusId.get(ce.internal_customer_id);
|
||||
if (list) list.push(hydrated);
|
||||
else looseCesByCusId.set(ce.internal_customer_id, [hydrated]);
|
||||
}
|
||||
|
||||
const cpsByCusId = new Map<string, any[]>();
|
||||
for (const cp of customer_products) {
|
||||
const hydrated = {
|
||||
...cp,
|
||||
customer_prices: cpricesByCpId.get(cp.id) ?? [],
|
||||
customer_entitlements: cesByCpId.get(cp.id) ?? [],
|
||||
free_trial: cp.free_trial_id ? (ftById.get(cp.free_trial_id) ?? null) : null,
|
||||
};
|
||||
const list = cpsByCusId.get(cp.internal_customer_id);
|
||||
if (list) list.push(hydrated);
|
||||
else cpsByCusId.set(cp.internal_customer_id, [hydrated]);
|
||||
}
|
||||
|
||||
const subsByCusId = new Map<string, any[]>();
|
||||
for (const cp of customer_products) {
|
||||
if (!cp.subscription_ids?.length) continue;
|
||||
const existing = subsByCusId.get(cp.internal_customer_id) ?? [];
|
||||
const seen = new Set(existing.map((s: any) => s.stripe_id));
|
||||
for (const subId of cp.subscription_ids) {
|
||||
if (seen.has(subId)) continue;
|
||||
const sub = subByStripeId.get(subId);
|
||||
if (sub) {
|
||||
existing.push(sub);
|
||||
seen.add(subId);
|
||||
}
|
||||
}
|
||||
if (existing.length) subsByCusId.set(cp.internal_customer_id, existing);
|
||||
}
|
||||
|
||||
const result: any[] = [];
|
||||
for (const c of customers) {
|
||||
result.push({
|
||||
...c,
|
||||
customer_products: cpsByCusId.get(c.internal_id) ?? [],
|
||||
extra_customer_entitlements: looseCesByCusId.get(c.internal_id) ?? [],
|
||||
subscriptions: subsByCusId.get(c.internal_id) ?? [],
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const runQueryInTxn = async ({
|
||||
db,
|
||||
query,
|
||||
}: {
|
||||
db: DB;
|
||||
query: SQL;
|
||||
}): Promise<Record<string, unknown>[]> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
await tx.execute(
|
||||
sql.raw(`SET LOCAL statement_timeout = ${STATEMENT_TIMEOUT_MS}`),
|
||||
);
|
||||
await tx.execute(sql.raw("SET LOCAL transaction_read_only = on"));
|
||||
const result = await tx.execute(query);
|
||||
return result as unknown as Record<string, unknown>[];
|
||||
});
|
||||
};
|
||||
|
||||
const summarise = (samples: number[]) => {
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
return {
|
||||
min: sorted[0] ?? 0,
|
||||
median: sorted[Math.floor(sorted.length / 2)] ?? 0,
|
||||
p95: sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0,
|
||||
max: sorted[sorted.length - 1] ?? 0,
|
||||
samples: sorted,
|
||||
};
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n================ Variant 07 + Reassembly Benchmark — ${LABEL} ================\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
try {
|
||||
console.log(chalk.cyan(`Org: ${ORG_ID} (${ENV})`));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Deep cursor: t=${DEEP_CURSOR.t} id=${DEEP_CURSOR.id} limit=${LIMIT}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
// Step 1 — EXPLAIN ANALYZE pass for server-side SQL execution time
|
||||
console.log(chalk.cyan("[1/3] EXPLAIN (ANALYZE, FORMAT JSON) — server-side ms"));
|
||||
const explainSamples: number[] = [];
|
||||
let lastExplainPlan: any = null;
|
||||
for (let i = 0; i < SQL_REPEATS; i++) {
|
||||
const explainRows = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${buildVariant07Sql()}`,
|
||||
});
|
||||
const plan = (explainRows[0] as { "QUERY PLAN": any })["QUERY PLAN"];
|
||||
const planObj = Array.isArray(plan) ? plan[0] : plan;
|
||||
const execTime = planObj["Execution Time"] as number;
|
||||
explainSamples.push(execTime);
|
||||
lastExplainPlan = planObj;
|
||||
process.stdout.write(chalk.gray(` run ${i + 1}: ${execTime.toFixed(2)}ms\n`));
|
||||
}
|
||||
const sqlSummary = summarise(explainSamples);
|
||||
|
||||
// Step 2 — actual query to capture rows for reassembly
|
||||
console.log(chalk.cyan("\n[2/3] Executing variant 07 to capture row payload"));
|
||||
const queryStart = performance.now();
|
||||
const rows = await runQueryInTxn({ db, query: buildVariant07Sql() });
|
||||
const queryWallMs = performance.now() - queryStart;
|
||||
const flatRow = rows[0] as unknown as V07Row;
|
||||
|
||||
const counts = {
|
||||
customers: flatRow.customers?.length ?? 0,
|
||||
customer_products: flatRow.customer_products?.length ?? 0,
|
||||
customer_entitlements: flatRow.customer_entitlements?.length ?? 0,
|
||||
extra_customer_entitlements: flatRow.extra_customer_entitlements?.length ?? 0,
|
||||
customer_prices: flatRow.customer_prices?.length ?? 0,
|
||||
entitlements: flatRow.entitlements?.length ?? 0,
|
||||
rollovers: flatRow.rollovers?.length ?? 0,
|
||||
replaceables: flatRow.replaceables?.length ?? 0,
|
||||
free_trials: flatRow.free_trials?.length ?? 0,
|
||||
subscriptions: flatRow.subscriptions?.length ?? 0,
|
||||
};
|
||||
console.log(
|
||||
chalk.gray(` client-side wall (incl. UK→US network): ${queryWallMs.toFixed(2)}ms`),
|
||||
);
|
||||
console.log(chalk.gray(` row counts: ${JSON.stringify(counts)}`));
|
||||
|
||||
const payloadBytes = Buffer.byteLength(JSON.stringify(flatRow), "utf8");
|
||||
console.log(
|
||||
chalk.gray(` payload size: ${(payloadBytes / 1024 / 1024).toFixed(2)} MB`),
|
||||
);
|
||||
|
||||
// Step 3 — reassembly benchmark (pure JS, in-memory)
|
||||
console.log(
|
||||
chalk.cyan(`\n[3/3] Reassembly benchmark — ${REASSEMBLY_REPEATS} runs`),
|
||||
);
|
||||
// Warm up the JIT
|
||||
for (let i = 0; i < 3; i++) reassemble(flatRow);
|
||||
const reassemblySamples: number[] = [];
|
||||
let lastReassembledLen = 0;
|
||||
for (let i = 0; i < REASSEMBLY_REPEATS; i++) {
|
||||
const start = performance.now();
|
||||
const reassembled = reassemble(flatRow);
|
||||
const elapsed = performance.now() - start;
|
||||
reassemblySamples.push(elapsed);
|
||||
lastReassembledLen = reassembled.length;
|
||||
}
|
||||
const reassemblySummary = summarise(reassemblySamples);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` reassembled ${lastReassembledLen} FullCustomers (${REASSEMBLY_REPEATS} runs)`,
|
||||
),
|
||||
);
|
||||
|
||||
// Report
|
||||
console.log(chalk.magentaBright("\n================ RESULTS ================\n"));
|
||||
console.log(chalk.bold("SQL (server-side, EXPLAIN ANALYZE Execution Time):"));
|
||||
console.log(
|
||||
` min=${sqlSummary.min.toFixed(2)}ms median=${sqlSummary.median.toFixed(2)}ms p95=${sqlSummary.p95.toFixed(2)}ms max=${sqlSummary.max.toFixed(2)}ms`,
|
||||
);
|
||||
console.log(chalk.gray(` samples: ${sqlSummary.samples.map((s) => s.toFixed(1)).join(", ")}`));
|
||||
console.log();
|
||||
console.log(chalk.bold("Reassembly (JS, in-memory, network-free):"));
|
||||
console.log(
|
||||
` min=${reassemblySummary.min.toFixed(2)}ms median=${reassemblySummary.median.toFixed(2)}ms p95=${reassemblySummary.p95.toFixed(2)}ms max=${reassemblySummary.max.toFixed(2)}ms`,
|
||||
);
|
||||
console.log(chalk.gray(` samples: ${reassemblySummary.samples.map((s) => s.toFixed(2)).join(", ")}`));
|
||||
console.log();
|
||||
console.log(chalk.bold.green("Total (network-free) median:"));
|
||||
console.log(
|
||||
chalk.bold.green(
|
||||
` ${(sqlSummary.median + reassemblySummary.median).toFixed(2)}ms = ${sqlSummary.median.toFixed(2)}ms SQL + ${reassemblySummary.median.toFixed(2)}ms reassembly`,
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
console.log(chalk.bold("Reference (UK→US client wall, includes ~100ms network):"));
|
||||
console.log(` one-shot db.execute: ${queryWallMs.toFixed(2)}ms`);
|
||||
console.log();
|
||||
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const reportPath = join(RESULTS_DIR, `${date}-v07-reassembly-${LABEL}.md`);
|
||||
const fixturePath = join(RESULTS_DIR, `${date}-v07-fixture-${LABEL}.json`);
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`# Variant 07 + JS Reassembly — ${date} — ${LABEL}`);
|
||||
lines.push("");
|
||||
lines.push("## Config");
|
||||
lines.push(`- org_id: \`${ORG_ID}\``);
|
||||
lines.push(`- env: \`${ENV}\``);
|
||||
lines.push(`- limit: ${LIMIT} (fetch ${FETCH_LIMIT})`);
|
||||
lines.push(`- deep cursor: \`{ t: ${DEEP_CURSOR.t}, id: ${DEEP_CURSOR.id} }\``);
|
||||
lines.push(`- SQL repeats: ${SQL_REPEATS}`);
|
||||
lines.push(`- reassembly repeats: ${REASSEMBLY_REPEATS} (after 3 warmup runs)`);
|
||||
lines.push("");
|
||||
lines.push("## SQL (server-side, EXPLAIN ANALYZE Execution Time, network-free)");
|
||||
lines.push("");
|
||||
lines.push(`| min | median | p95 | max |`);
|
||||
lines.push(`|---|---|---|---|`);
|
||||
lines.push(
|
||||
`| ${sqlSummary.min.toFixed(2)}ms | ${sqlSummary.median.toFixed(2)}ms | ${sqlSummary.p95.toFixed(2)}ms | ${sqlSummary.max.toFixed(2)}ms |`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`Samples: ${sqlSummary.samples.map((s) => `${s.toFixed(1)}ms`).join(", ")}`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("## Reassembly (JS, in-memory)");
|
||||
lines.push("");
|
||||
lines.push(`| min | median | p95 | max |`);
|
||||
lines.push(`|---|---|---|---|`);
|
||||
lines.push(
|
||||
`| ${reassemblySummary.min.toFixed(2)}ms | ${reassemblySummary.median.toFixed(2)}ms | ${reassemblySummary.p95.toFixed(2)}ms | ${reassemblySummary.max.toFixed(2)}ms |`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`Samples: ${reassemblySummary.samples.map((s) => `${s.toFixed(2)}ms`).join(", ")}`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("## Total network-free");
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`**${(sqlSummary.median + reassemblySummary.median).toFixed(2)}ms** = ${sqlSummary.median.toFixed(2)}ms SQL + ${reassemblySummary.median.toFixed(2)}ms reassembly`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("## Row counts");
|
||||
lines.push("");
|
||||
lines.push("```json");
|
||||
lines.push(JSON.stringify(counts, null, 2));
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
lines.push(`Payload size: ${(payloadBytes / 1024 / 1024).toFixed(2)} MB`);
|
||||
lines.push("");
|
||||
lines.push("## Reference: UK→US client wall");
|
||||
lines.push("");
|
||||
lines.push(`One-shot \`db.execute\`: ${queryWallMs.toFixed(2)}ms`);
|
||||
lines.push("");
|
||||
lines.push("## Last EXPLAIN plan (JSON)");
|
||||
lines.push("");
|
||||
lines.push("```json");
|
||||
lines.push(JSON.stringify(lastExplainPlan, null, 2));
|
||||
lines.push("```");
|
||||
|
||||
writeFileSync(reportPath, lines.join("\n"));
|
||||
writeFileSync(fixturePath, JSON.stringify(flatRow));
|
||||
|
||||
console.log(chalk.green(`✅ Report written to ${reportPath}`));
|
||||
console.log(chalk.green(`✅ Fixture written to ${fixturePath}`));
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Benchmark Complete ================\n",
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red("\n❌ Benchmark failed:"));
|
||||
console.error(error);
|
||||
console.error(chalk.gray(JSON.stringify(error, Object.getOwnPropertyNames(error || {}))));
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,547 +0,0 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
AllowanceType,
|
||||
type ApiCustomerV5,
|
||||
ApiVersion,
|
||||
ApiVersionClass,
|
||||
applyResponseVersionChanges,
|
||||
AppEnv,
|
||||
BillingInterval,
|
||||
CollectionMethod,
|
||||
type CustomerLegacyData,
|
||||
CusProductStatus,
|
||||
EntInterval,
|
||||
FeatureType,
|
||||
type FullCustomer,
|
||||
type FullCustomerEntitlement,
|
||||
type FullCusProduct
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { getApiCustomerBase } from "../src/internal/customers/cusUtils/apiCusUtils/getApiCustomerBase";
|
||||
|
||||
const CUSTOMER_COUNT = 1000;
|
||||
const ITERATIONS = 5;
|
||||
const WARMUP_ITERATIONS = 2;
|
||||
|
||||
const ORG_ID = "org_bench";
|
||||
const FEATURE_IDS = [
|
||||
"messages",
|
||||
"credits",
|
||||
"users",
|
||||
"projects",
|
||||
"api_calls",
|
||||
"storage_gb",
|
||||
"connectors",
|
||||
"webhooks",
|
||||
"exports",
|
||||
"dashboards",
|
||||
];
|
||||
const ADDON_FEATURE_IDS = [
|
||||
"extra_messages",
|
||||
"extra_credits",
|
||||
"premium_support",
|
||||
"sso",
|
||||
"audit_log",
|
||||
];
|
||||
const LOOSE_FEATURE_IDS = [
|
||||
"loose_a",
|
||||
"loose_b",
|
||||
"loose_c",
|
||||
"loose_d",
|
||||
"loose_e",
|
||||
];
|
||||
|
||||
const featureFromId = (id: string) => ({
|
||||
internal_id: `fe_${id}`,
|
||||
org_id: ORG_ID,
|
||||
created_at: 1000,
|
||||
env: AppEnv.Sandbox,
|
||||
id,
|
||||
name: id,
|
||||
type: FeatureType.Metered,
|
||||
config: { usage_type: "single_use" },
|
||||
display: { singular: id, plural: `${id}s` },
|
||||
archived: false,
|
||||
event_names: [id],
|
||||
});
|
||||
|
||||
const allFeatures = [
|
||||
...FEATURE_IDS.map(featureFromId),
|
||||
...ADDON_FEATURE_IDS.map(featureFromId),
|
||||
...LOOSE_FEATURE_IDS.map(featureFromId),
|
||||
];
|
||||
|
||||
const makeEntitlement = ({
|
||||
cusEntId,
|
||||
productInternalId,
|
||||
productCusId,
|
||||
featureId,
|
||||
internalCustomerId,
|
||||
customerId,
|
||||
}: {
|
||||
cusEntId: string;
|
||||
productInternalId: string;
|
||||
productCusId: string | null;
|
||||
featureId: string;
|
||||
internalCustomerId: string;
|
||||
customerId: string;
|
||||
}): FullCustomerEntitlement => ({
|
||||
id: cusEntId,
|
||||
internal_customer_id: internalCustomerId,
|
||||
internal_entity_id: null,
|
||||
internal_feature_id: `fe_${featureId}`,
|
||||
customer_id: customerId,
|
||||
feature_id: featureId,
|
||||
customer_product_id: productCusId,
|
||||
entitlement_id: `ent_${featureId}_${productInternalId}`,
|
||||
created_at: 1000,
|
||||
unlimited: false,
|
||||
balance: 100,
|
||||
additional_balance: 0,
|
||||
usage_allowed: null,
|
||||
next_reset_at: 2000,
|
||||
adjustment: 0,
|
||||
expires_at: null,
|
||||
cache_version: 0,
|
||||
entities: null,
|
||||
external_id: null,
|
||||
entitlement: {
|
||||
id: `ent_${featureId}_${productInternalId}`,
|
||||
created_at: 1000,
|
||||
internal_feature_id: `fe_${featureId}`,
|
||||
internal_product_id: productInternalId,
|
||||
is_custom: false,
|
||||
allowance_type: AllowanceType.Fixed,
|
||||
allowance: 100,
|
||||
interval: EntInterval.Month,
|
||||
interval_count: 1,
|
||||
entity_feature_id: null,
|
||||
feature_id: featureId,
|
||||
usage_limit: null,
|
||||
feature: featureFromId(featureId),
|
||||
},
|
||||
replaceables: [],
|
||||
rollovers: [],
|
||||
});
|
||||
|
||||
const makeCusProduct = ({
|
||||
productCusId,
|
||||
productInternalId,
|
||||
productId,
|
||||
productName,
|
||||
isAddOn,
|
||||
internalCustomerId,
|
||||
customerId,
|
||||
featureIds,
|
||||
cusEntPrefix,
|
||||
internalEntityId,
|
||||
entityId,
|
||||
}: {
|
||||
productCusId: string;
|
||||
productInternalId: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
isAddOn: boolean;
|
||||
internalCustomerId: string;
|
||||
customerId: string;
|
||||
featureIds: string[];
|
||||
cusEntPrefix: string;
|
||||
internalEntityId?: string | null;
|
||||
entityId?: string | null;
|
||||
}): FullCusProduct => ({
|
||||
id: productCusId,
|
||||
internal_product_id: productInternalId,
|
||||
product_id: productId,
|
||||
internal_customer_id: internalCustomerId,
|
||||
customer_id: customerId,
|
||||
internal_entity_id: internalEntityId ?? null,
|
||||
entity_id: entityId ?? null,
|
||||
created_at: 1500,
|
||||
updated_at: null,
|
||||
status: CusProductStatus.Active,
|
||||
canceled: false,
|
||||
starts_at: 1500,
|
||||
access_starts_at: null,
|
||||
trial_ends_at: null,
|
||||
billing_cycle_anchor_resets_at: null,
|
||||
canceled_at: null,
|
||||
ended_at: null,
|
||||
options: [],
|
||||
free_trial_id: null,
|
||||
collection_method: CollectionMethod.ChargeAutomatically,
|
||||
subscription_ids: [`sub_${productCusId}`],
|
||||
scheduled_ids: [],
|
||||
processor: { type: "stripe" as any },
|
||||
quantity: 1,
|
||||
api_semver: null,
|
||||
is_custom: false,
|
||||
billing_version: "v2" as any,
|
||||
external_id: null,
|
||||
stripe_checkout_session_id: null,
|
||||
customer_prices: [
|
||||
{
|
||||
id: `cus_price_${productCusId}`,
|
||||
internal_customer_id: internalCustomerId,
|
||||
customer_product_id: productCusId,
|
||||
created_at: 1500,
|
||||
price_id: `price_${productInternalId}`,
|
||||
price: {
|
||||
id: `price_${productInternalId}`,
|
||||
internal_product_id: productInternalId,
|
||||
org_id: ORG_ID,
|
||||
created_at: 1500,
|
||||
billing_type: null,
|
||||
tier_behavior: null,
|
||||
is_custom: false,
|
||||
config: {
|
||||
type: "fixed",
|
||||
amount: 99,
|
||||
interval: BillingInterval.Month,
|
||||
} as any,
|
||||
entitlement_id: null,
|
||||
proration_config: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
customer_entitlements: featureIds.map((fid, i) =>
|
||||
makeEntitlement({
|
||||
cusEntId: `${cusEntPrefix}_${i}`,
|
||||
productInternalId,
|
||||
productCusId,
|
||||
featureId: fid,
|
||||
internalCustomerId,
|
||||
customerId,
|
||||
}),
|
||||
),
|
||||
product: {
|
||||
id: productId,
|
||||
name: productName,
|
||||
description: null,
|
||||
is_add_on: isAddOn,
|
||||
is_default: false,
|
||||
version: 1,
|
||||
group: "",
|
||||
env: AppEnv.Sandbox,
|
||||
internal_id: productInternalId,
|
||||
org_id: ORG_ID,
|
||||
created_at: 1000,
|
||||
processor: { type: "stripe", id: `prod_${productId}` },
|
||||
base_variant_id: null,
|
||||
archived: false,
|
||||
config: { ignore_past_due: false },
|
||||
},
|
||||
free_trial: null,
|
||||
});
|
||||
|
||||
const makeFullCustomer = (idx: number): FullCustomer => {
|
||||
const internalId = `cus_bench_${idx.toString().padStart(8, "0")}`;
|
||||
const customerId = `bench_${idx}`;
|
||||
|
||||
const customerProducts: FullCusProduct[] = [
|
||||
makeCusProduct({
|
||||
productCusId: `cus_prod_main_${idx}`,
|
||||
productInternalId: "prod_main",
|
||||
productId: "pro",
|
||||
productName: "Pro",
|
||||
isAddOn: false,
|
||||
internalCustomerId: internalId,
|
||||
customerId,
|
||||
featureIds: FEATURE_IDS,
|
||||
cusEntPrefix: `cus_ent_main_${idx}`,
|
||||
}),
|
||||
makeCusProduct({
|
||||
productCusId: `cus_prod_addon_${idx}`,
|
||||
productInternalId: "prod_addon",
|
||||
productId: "pro_addon",
|
||||
productName: "Pro Add-on",
|
||||
isAddOn: true,
|
||||
internalCustomerId: internalId,
|
||||
customerId,
|
||||
featureIds: ADDON_FEATURE_IDS,
|
||||
cusEntPrefix: `cus_ent_addon_${idx}`,
|
||||
}),
|
||||
];
|
||||
|
||||
const entities = Array.from({ length: 5 }, (_, i) => ({
|
||||
id: `ent_${idx}_${i}`,
|
||||
org_id: ORG_ID,
|
||||
created_at: 1100,
|
||||
internal_id: `ent_int_${idx}_${i}`,
|
||||
internal_customer_id: internalId,
|
||||
env: AppEnv.Sandbox,
|
||||
name: `Entity ${i}`,
|
||||
deleted: false,
|
||||
feature_id: "seats",
|
||||
internal_feature_id: "fe_seats",
|
||||
}));
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const ent = entities[i]!;
|
||||
customerProducts.push(
|
||||
makeCusProduct({
|
||||
productCusId: `cus_prod_main_${idx}_e${i}`,
|
||||
productInternalId: "prod_main",
|
||||
productId: "pro",
|
||||
productName: "Pro (Entity-scoped)",
|
||||
isAddOn: false,
|
||||
internalCustomerId: internalId,
|
||||
customerId,
|
||||
featureIds: FEATURE_IDS,
|
||||
cusEntPrefix: `cus_ent_main_${idx}_e${i}`,
|
||||
internalEntityId: ent.internal_id,
|
||||
entityId: ent.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const looseEnts: FullCustomerEntitlement[] = LOOSE_FEATURE_IDS.map((fid, i) =>
|
||||
makeEntitlement({
|
||||
cusEntId: `cus_ent_loose_${idx}_${i}`,
|
||||
productInternalId: "prod_loose",
|
||||
productCusId: null,
|
||||
featureId: fid,
|
||||
internalCustomerId: internalId,
|
||||
customerId,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
id: customerId,
|
||||
name: `Bench Customer ${idx}`,
|
||||
email: `bench+${idx}@autumn.dev`,
|
||||
fingerprint: null,
|
||||
internal_id: internalId,
|
||||
org_id: ORG_ID,
|
||||
created_at: 1000,
|
||||
env: AppEnv.Sandbox,
|
||||
processor: { id: `cus_stripe_${idx}`, type: "stripe" },
|
||||
processors: null,
|
||||
metadata: {},
|
||||
send_email_receipts: false,
|
||||
auto_topups: null,
|
||||
spend_limits: null,
|
||||
usage_alerts: null,
|
||||
overage_allowed: null,
|
||||
config: null,
|
||||
customer_products: customerProducts,
|
||||
entities,
|
||||
extra_customer_entitlements: looseEnts,
|
||||
subscriptions: customerProducts
|
||||
.filter((cp) => cp.subscription_ids?.length)
|
||||
.flatMap((cp) =>
|
||||
(cp.subscription_ids ?? []).map((sid) => ({
|
||||
id: sid,
|
||||
stripe_id: sid,
|
||||
stripe_schedule_id: null,
|
||||
created_at: 1500,
|
||||
usage_features: [],
|
||||
metadata: {},
|
||||
org_id: ORG_ID,
|
||||
env: AppEnv.Sandbox,
|
||||
current_period_start: 1500,
|
||||
current_period_end: 4000,
|
||||
})),
|
||||
),
|
||||
} as unknown as FullCustomer;
|
||||
};
|
||||
|
||||
const makeCtx = () =>
|
||||
({
|
||||
org: {
|
||||
id: ORG_ID,
|
||||
slug: "bench",
|
||||
created_at: 1000,
|
||||
default_currency: "usd",
|
||||
config: { in_statuses: [CusProductStatus.Active] },
|
||||
stripe_connected: false,
|
||||
},
|
||||
env: AppEnv.Sandbox,
|
||||
features: allFeatures,
|
||||
logger: {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
warn: () => {},
|
||||
debug: () => {},
|
||||
trace: () => {},
|
||||
child: () => makeCtx().logger,
|
||||
},
|
||||
expand: [],
|
||||
apiVersion: new ApiVersionClass(ApiVersion.V2_3),
|
||||
}) as any;
|
||||
|
||||
const summarize = (samples: number[]) => {
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
return {
|
||||
min: sorted[0] ?? 0,
|
||||
p50: sorted[Math.floor(sorted.length / 2)] ?? 0,
|
||||
p95: sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0,
|
||||
max: sorted[sorted.length - 1] ?? 0,
|
||||
mean: samples.reduce((a, b) => a + b, 0) / samples.length,
|
||||
};
|
||||
};
|
||||
|
||||
const fmt = (n: number) => `${n.toFixed(2)}ms`.padStart(9, " ");
|
||||
|
||||
const runSerial = async (customers: FullCustomer[], ctx: any) => {
|
||||
const finals: ApiCustomerV5[] = [];
|
||||
for (const fullCus of customers) {
|
||||
const { apiCustomer: baseCustomer, legacyData } = await getApiCustomerBase({
|
||||
ctx,
|
||||
fullCus,
|
||||
withAutumnId: false,
|
||||
});
|
||||
const versioned = applyResponseVersionChanges<ApiCustomerV5, CustomerLegacyData>({
|
||||
input: baseCustomer,
|
||||
legacyData,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Customer,
|
||||
ctx,
|
||||
});
|
||||
finals.push(versioned);
|
||||
}
|
||||
return finals;
|
||||
};
|
||||
|
||||
const runPromiseAll = async (customers: FullCustomer[], ctx: any) => {
|
||||
const finals = await Promise.all(
|
||||
customers.map(async (fullCus) => {
|
||||
const { apiCustomer: baseCustomer, legacyData } = await getApiCustomerBase({
|
||||
ctx,
|
||||
fullCus,
|
||||
withAutumnId: false,
|
||||
});
|
||||
return applyResponseVersionChanges<ApiCustomerV5, CustomerLegacyData>({
|
||||
input: baseCustomer,
|
||||
legacyData,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Customer,
|
||||
ctx,
|
||||
});
|
||||
}),
|
||||
);
|
||||
return finals;
|
||||
};
|
||||
|
||||
// "sync-equivalent" path: keeps the async function calls but uses a tight
|
||||
// promise-chained sequence with no extra microtask churn between customers.
|
||||
// Since getApiCustomerBase is async, true sync isn't possible without rewriting it.
|
||||
const runSyncEquivalent = (customers: FullCustomer[], ctx: any) => {
|
||||
let p: Promise<ApiCustomerV5[]> = Promise.resolve([] as ApiCustomerV5[]);
|
||||
for (const fullCus of customers) {
|
||||
p = p.then(async (acc) => {
|
||||
const { apiCustomer: baseCustomer, legacyData } = await getApiCustomerBase({
|
||||
ctx,
|
||||
fullCus,
|
||||
withAutumnId: false,
|
||||
});
|
||||
const versioned = applyResponseVersionChanges<ApiCustomerV5, CustomerLegacyData>({
|
||||
input: baseCustomer,
|
||||
legacyData,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Customer,
|
||||
ctx,
|
||||
});
|
||||
acc.push(versioned);
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
const benchVariant = async ({
|
||||
name,
|
||||
customers,
|
||||
ctx,
|
||||
run,
|
||||
}: {
|
||||
name: string;
|
||||
customers: FullCustomer[];
|
||||
ctx: any;
|
||||
run: (customers: FullCustomer[], ctx: any) => Promise<ApiCustomerV5[]>;
|
||||
}) => {
|
||||
for (let i = 0; i < WARMUP_ITERATIONS; i++) await run(customers, ctx);
|
||||
|
||||
const samples: number[] = [];
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const t0 = performance.now();
|
||||
const result = await run(customers, ctx);
|
||||
const t1 = performance.now();
|
||||
if (result.length !== customers.length) {
|
||||
throw new Error(`${name}: result length mismatch ${result.length} vs ${customers.length}`);
|
||||
}
|
||||
samples.push(t1 - t0);
|
||||
}
|
||||
|
||||
const s = summarize(samples);
|
||||
const perCustomer = s.p50 / customers.length;
|
||||
console.log(
|
||||
` ${chalk.cyan(name.padEnd(20))} | total p50 ${fmt(s.p50)} (per cus ${perCustomer.toFixed(3)}ms) | mean ${fmt(s.mean)} | p95 ${fmt(s.p95)} | min ${fmt(s.min)} | max ${fmt(s.max)}`,
|
||||
);
|
||||
return s;
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n================ Version Pipeline Bench ================`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` customers per iter: ${CUSTOMER_COUNT.toLocaleString()}, iterations: ${ITERATIONS} (+${WARMUP_ITERATIONS} warmup)\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const ctx = makeCtx();
|
||||
const customers = Array.from({ length: CUSTOMER_COUNT }, (_, i) =>
|
||||
makeFullCustomer(i),
|
||||
);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` shape per customer: ${customers[0]!.customer_products.length} cps · ${customers[0]!.customer_products.reduce((n, cp) => n + cp.customer_entitlements.length, 0)} bound ces · ${customers[0]!.extra_customer_entitlements.length} loose ces · ${customers[0]!.entities.length} entities`,
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
|
||||
console.log(chalk.bold("Baseline (serial for-await):"));
|
||||
const serial = await benchVariant({ name: "serial", customers, ctx, run: runSerial });
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold("Variant A (Promise.all):"));
|
||||
const promiseAll = await benchVariant({
|
||||
name: "Promise.all",
|
||||
customers,
|
||||
ctx,
|
||||
run: runPromiseAll,
|
||||
});
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold("Variant B (sync-equivalent promise chain):"));
|
||||
const syncish = await benchVariant({
|
||||
name: "sync-equivalent",
|
||||
customers,
|
||||
ctx,
|
||||
run: runSyncEquivalent,
|
||||
});
|
||||
|
||||
const fmtDelta = (other: number, baseline: number) => {
|
||||
const delta = ((other - baseline) / baseline) * 100;
|
||||
const arrow = delta < 0 ? "↓" : "↑";
|
||||
return `${arrow}${Math.abs(delta).toFixed(1)}%`;
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log(chalk.magentaBright("================ Summary ================"));
|
||||
console.log(
|
||||
` serial: ${fmt(serial.p50)} ${chalk.gray("(baseline)")}`,
|
||||
);
|
||||
console.log(
|
||||
` Promise.all: ${fmt(promiseAll.p50)} (${fmtDelta(promiseAll.p50, serial.p50)})`,
|
||||
);
|
||||
console.log(
|
||||
` sync-equivalent: ${fmt(syncish.p50)} (${fmtDelta(syncish.p50, serial.p50)})`,
|
||||
);
|
||||
};
|
||||
|
||||
await main();
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user