Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/backend/scripts/verify-data-integrity/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { toQueryableSqlQuery } from "@/lib/bulldozer/db/index";
import { tableIdToDebugString } from "@/lib/bulldozer/db/utilities";
import { syncExternalDatabases } from "@/lib/external-db-sync";
import { createPaymentsSchema } from "@/lib/payments/schema/index";
import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "@/lib/tenancies";
import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client";
import type { OrganizationRenderedConfig } from "@stackframe/stack-shared/dist/config/schema";
Expand Down Expand Up @@ -168,6 +171,21 @@ async function main() {
console.log(`Will check at most ${maxUsersPerProject} users per project.`);
}

await recurse(`[bulldozer] verifying data integrity across all payments tables`, async () => {
const schema = createPaymentsSchema();
for (const table of schema._allTables) {
const label = tableIdToDebugString(table.tableId);
await recurse(`[bulldozer table] ${label}`, async () => {
const errors = await prismaClient.$queryRawUnsafe<unknown[]>(toQueryableSqlQuery(table.verifyDataIntegrity()));
if (errors.length > 0) {
throw new StackAssertionError(deindent`
Bulldozer data integrity violation in table ${label}: found ${errors.length} error row(s).
`, { errors });
}
});
}
});

const endAt = Math.min(startAt + count, projects.length);
for (let i = startAt; i < endAt; i++) {
const projectId = projects[i].id;
Expand Down
51 changes: 49 additions & 2 deletions apps/backend/src/lib/bulldozer/db/index.fuzz.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings";
import postgres from "postgres";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
import { declareCompactTable, declareConcatTable, declareFilterTable, declareFlatMapTable, declareGroupByTable, declareLeftJoinTable, declareLFoldTable, declareLimitTable, declareMapTable, declareReduceTable, declareSortTable, declareStoredTable, declareTimeFoldTable, toExecutableSqlTransaction, toQueryableSqlQuery } from "./index";
import type { Table } from "./index";
import {
declareCompactTable as _declareCompactTable,
declareConcatTable as _declareConcatTable,
declareFilterTable as _declareFilterTable,
declareFlatMapTable as _declareFlatMapTable,
declareGroupByTable as _declareGroupByTable,
declareLeftJoinTable as _declareLeftJoinTable,
declareLFoldTable as _declareLFoldTable,
declareLimitTable as _declareLimitTable,
declareMapTable as _declareMapTable,
declareReduceTable as _declareReduceTable,
declareSortTable as _declareSortTable,
declareStoredTable as _declareStoredTable,
declareTimeFoldTable as _declareTimeFoldTable,
toExecutableSqlTransaction,
toQueryableSqlQuery,
} from "./index";

// any is used here because the verifier works with heterogeneous table types
const allInitializedTables: Table<any, any, any>[] = [];
function trackTable<T extends Table<any, any, any>>(table: T): T {
allInitializedTables.push(table);
return table;
}
function tracked<Fn extends (...args: any[]) => Table<any, any, any>>(fn: Fn): Fn {
return ((...args: unknown[]) => trackTable(fn(...args))) as Fn;
}

const declareCompactTable = tracked(_declareCompactTable);
const declareConcatTable = tracked(_declareConcatTable);
const declareFilterTable = tracked(_declareFilterTable);
const declareFlatMapTable = tracked(_declareFlatMapTable);
const declareGroupByTable = tracked(_declareGroupByTable);
const declareLeftJoinTable = tracked(_declareLeftJoinTable);
const declareLFoldTable = tracked(_declareLFoldTable);
const declareLimitTable = tracked(_declareLimitTable);
const declareMapTable = tracked(_declareMapTable);
const declareReduceTable = tracked(_declareReduceTable);
const declareSortTable = tracked(_declareSortTable);
const declareStoredTable = tracked(_declareStoredTable);
const declareTimeFoldTable = tracked(_declareTimeFoldTable);

type TestDb = { full: string, base: string };

Expand Down Expand Up @@ -675,7 +716,13 @@ describe.sequential("bulldozer db fuzz composition (real postgres)", () => {
`;
});

afterEach(() => {
afterEach(async () => {
for (const table of allInitializedTables) {
const errors = await readRows(table.verifyDataIntegrity(), "afterEach.verifyDataIntegrity");
expect(errors).toEqual([]);
}
allInitializedTables.length = 0;

if (!FUZZ_TRACE_ENABLED) return;
const testName = getCurrentTestNameForTrace();
const bucket = tracesByTest.get(testName);
Expand Down
53 changes: 51 additions & 2 deletions apps/backend/src/lib/bulldozer/db/index.perf.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings";
import postgres from "postgres";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { declareCompactTable, declareConcatTable, declareFilterTable, declareFlatMapTable, declareGroupByTable, declareLeftJoinTable, declareLFoldTable, declareLimitTable, declareMapTable, declareReduceTable, declareSortTable, declareStoredTable, declareTimeFoldTable, toExecutableSqlTransaction, toQueryableSqlQuery } from "./index";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { Table } from "./index";
import {
declareCompactTable as _declareCompactTable,
declareConcatTable as _declareConcatTable,
declareFilterTable as _declareFilterTable,
declareFlatMapTable as _declareFlatMapTable,
declareGroupByTable as _declareGroupByTable,
declareLeftJoinTable as _declareLeftJoinTable,
declareLFoldTable as _declareLFoldTable,
declareLimitTable as _declareLimitTable,
declareMapTable as _declareMapTable,
declareReduceTable as _declareReduceTable,
declareSortTable as _declareSortTable,
declareStoredTable as _declareStoredTable,
declareTimeFoldTable as _declareTimeFoldTable,
toExecutableSqlTransaction,
toQueryableSqlQuery,
} from "./index";

// any is used here because the verifier works with heterogeneous table types
const allInitializedTables: Table<any, any, any>[] = [];
function trackTable<T extends Table<any, any, any>>(table: T): T {
allInitializedTables.push(table);
return table;
}
function tracked<Fn extends (...args: any[]) => Table<any, any, any>>(fn: Fn): Fn {
return ((...args: unknown[]) => trackTable(fn(...args))) as Fn;
}

const declareCompactTable = tracked(_declareCompactTable);
const declareConcatTable = tracked(_declareConcatTable);
const declareFilterTable = tracked(_declareFilterTable);
const declareFlatMapTable = tracked(_declareFlatMapTable);
const declareGroupByTable = tracked(_declareGroupByTable);
const declareLeftJoinTable = tracked(_declareLeftJoinTable);
const declareLFoldTable = tracked(_declareLFoldTable);
const declareLimitTable = tracked(_declareLimitTable);
const declareMapTable = tracked(_declareMapTable);
const declareReduceTable = tracked(_declareReduceTable);
const declareSortTable = tracked(_declareSortTable);
const declareStoredTable = tracked(_declareStoredTable);
const declareTimeFoldTable = tracked(_declareTimeFoldTable);

type TestDb = { full: string, base: string };
type SqlExpression<T> = { type: "expression", sql: string };
Expand Down Expand Up @@ -315,6 +356,14 @@
`;
});

afterEach(async () => {
for (const table of allInitializedTables) {
const errors = await readRows(table.verifyDataIntegrity());
expect(errors).toEqual([]);

Check failure on line 362 in apps/backend/src/lib/bulldozer/db/index.perf.test.ts

View workflow job for this annotation

GitHub Actions / E2E Tests (Local Emulator, Node 22.x)

src/lib/bulldozer/db/index.perf.test.ts > bulldozer db performance (real postgres) > load test: prefilled stored table with hundreds of thousands of rows stays functional and fast (50000 rows)

AssertionError: expected [ …(50000) ] to deeply equal [] - Expected + Received - Array [] + Array [ + Object { + "actual": Object { + "team": "beta", + "value": 254, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-2254", + }, + Object { + "actual": Object { + "team": "beta", + "value": 818, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-10818", + }, + Object { + "actual": Object { + "team": "beta", + "value": 22, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-8022", + }, + Object { + "actual": Object { + "team": "beta", + "value": 866, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-866", + }, + Object { + "actual": Object { + "team": "beta", + "value": 338, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-11338", + }, + Object { + "actual": Object { + "team": "beta", + "value": 662, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-4662", + }, + Object { + "actual": Object { + "team": "beta", + "value": 10, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-4010", + }, + Object { + "actual": Object { + "team": "beta", + "value": 850, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-4850", + }, + Object { + "actual": Object { + "team": "beta", + "value": 202, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-3202", + }, + Object { + "actual": Object { + "team": "beta", + "value": 214, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-3214", + }, + Object { + "actual": Object { + "team": "beta", + "value": 926, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-7926", + }, + Object { + "actual": Object { + "team": "beta", + "value": 642, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-642", + }, + Object { + "actual": Object { + "team": "beta", + "value": 42, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-1042", + }, + Object { + "actual": Object { + "team": "beta", + "value": 606, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-5606", + }, + Object { + "actual": Object { + "team": "beta", + "value": 810, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-11810", + }, + Object { + "actual": Object { + "team": "beta", + "value": 270, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-7270", + }, + Object { + "actual": Object { + "team": "beta", + "value": 54, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-3054", + }, + Object { + "actual": Object { + "team": "beta", + "value": 566, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-8566", + }, + Object { + "actual": Object { + "team": "beta", + "

Check failure on line 362 in apps/backend/src/lib/bulldozer/db/index.perf.test.ts

View workflow job for this annotation

GitHub Actions / E2E Tests (Local Emulator, Node 22.x)

src/lib/bulldozer/db/index.perf.test.ts > bulldozer db performance (real postgres) > load test: prefilled stored table with hundreds of thousands of rows stays functional and fast (20000 rows)

AssertionError: expected [ …(20000) ] to deeply equal [] - Expected + Received - Array [] + Array [ + Object { + "actual": Object { + "team": null, + "value": 496, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-4496", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 803, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-15803", + }, + Object { + "actual": Object { + "team": null, + "value": 724, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-15724", + }, + Object { + "actual": Object { + "team": "alpha", + "value": 525, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "alpha", + "rowidentifier": "seed-17525", + }, + Object { + "actual": Object { + "team": null, + "value": 856, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-4856", + }, + Object { + "actual": Object { + "team": "alpha", + "value": 797, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "alpha", + "rowidentifier": "seed-14797", + }, + Object { + "actual": Object { + "team": null, + "value": 20, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-7020", + }, + Object { + "actual": Object { + "team": "beta", + "value": 890, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-15890", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 315, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-12315", + }, + Object { + "actual": Object { + "team": "beta", + "value": 222, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-15222", + }, + Object { + "actual": Object { + "team": null, + "value": 72, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-12072", + }, + Object { + "actual": Object { + "team": "beta", + "value": 886, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-15886", + }, + Object { + "actual": Object { + "team": "alpha", + "value": 261, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "alpha", + "rowidentifier": "seed-5261", + }, + Object { + "actual": Object { + "team": "beta", + "value": 138, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-5138", + }, + Object { + "actual": Object { + "team": "alpha", + "value": 173, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "alpha", + "rowidentifier": "seed-1173", + }, + Object { + "actual": Object { + "team": "alpha", + "value": 113, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "alpha", + "rowidentifier": "seed-2113", + }, + Object { + "actual": Object { + "team": null, + "value": 156, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-7156", + }, + Object { + "actual": Object { + "team": "beta", + "value": 282, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "beta", + "rowidentifier": "seed-9282", + }, + Object { + "actual": Object { + "team": null, + "va

Check failure on line 362 in apps/backend/src/lib/bulldozer/db/index.perf.test.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

src/lib/bulldozer/db/index.perf.test.ts > bulldozer db performance (real postgres) > load test: prefilled stored table with hundreds of thousands of rows stays functional and fast (50000 rows)

AssertionError: expected [ …(50000) ] to deeply equal [] - Expected + Received - Array [] + Array [ + Object { + "actual": Object { + "team": "gamma", + "value": 879, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-5879", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 447, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-4447", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 579, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-2579", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 523, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-1523", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 879, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-2879", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 875, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-7875", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 611, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-11611", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 855, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-11855", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 663, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-10663", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 19, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-13019", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 87, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-6087", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 323, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-8323", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 887, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-7887", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 811, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-11811", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 947, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-6947", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 975, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-10975", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 711, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-8711", + }, + Object { + "actual": Object { + "team": "gamma", + "value": 239, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": "gamma", + "rowidentifier": "seed-3239", + }, + Object { + "actual":

Check failure on line 362 in apps/backend/src/lib/bulldozer/db/index.perf.test.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

src/lib/bulldozer/db/index.perf.test.ts > bulldozer db performance (real postgres) > load test: prefilled stored table with hundreds of thousands of rows stays functional and fast (20000 rows)

AssertionError: expected [ …(20000) ] to deeply equal [] - Expected + Received - Array [] + Array [ + Object { + "actual": Object { + "team": null, + "value": 832, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-9832", + }, + Object { + "actual": Object { + "team": null, + "value": 896, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-10896", + }, + Object { + "actual": Object { + "team": null, + "value": 112, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-11112", + }, + Object { + "actual": Object { + "team": null, + "value": 440, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-6440", + }, + Object { + "actual": Object { + "team": null, + "value": 432, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-432", + }, + Object { + "actual": Object { + "team": null, + "value": 532, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-6532", + }, + Object { + "actual": Object { + "team": null, + "value": 964, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-7964", + }, + Object { + "actual": Object { + "team": null, + "value": 80, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-10080", + }, + Object { + "actual": Object { + "team": null, + "value": 284, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-9284", + }, + Object { + "actual": Object { + "team": null, + "value": 780, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-1780", + }, + Object { + "actual": Object { + "team": null, + "value": 720, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-4720", + }, + Object { + "actual": Object { + "team": null, + "value": 480, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-480", + }, + Object { + "actual": Object { + "team": null, + "value": 672, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-1672", + }, + Object { + "actual": Object { + "team": null, + "value": 644, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-12644", + }, + Object { + "actual": Object { + "team": null, + "value": 416, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-6416", + }, + Object { + "actual": Object { + "team": null, + "value": 228, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-4228", + }, + Object { + "actual": Object { + "team": null, + "value": 428, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-8428", + }, + Object { + "actual": Object { + "team": null, + "value": 196, + }, + "errortype": "extra_row", + "expected": null, + "groupkey": null, + "rowidentifier": "seed-9196", + }, + Object { + "actual": Object { + "team": null, + "value": 28, + }, + "errortype": "extra_row", + "expected":
}
allInitializedTables.length = 0;
});

afterAll(async () => {
await sql.end();
await adminSql.unsafe(`
Expand Down
Loading
Loading