/** * TASK-0135 (Phase 15, hot path #2 — the editor autosave flush): the N+1 * elimination query-shape test. * * Spec (docs/tasks/phase-15-production.md): "single `createMany`/`updateMany` * per collection per flush — the profiled N+1 is fixed; test asserts * query-count flat across batch size 10/100/1000 blocks". * * The profiled shape was ONE `tx.block.upsert` PER BLOCK (N round-trips per * flush — up to 5000 per the DTO cap). The fix upserts the whole set in ONE * `INSERT … ON CONFLICT (id) DO UPDATE`, so the flush issues a CONSTANT * number of statements regardless of block count: * 1. page.findUnique (the pre-tx version check) * 2. block.findMany (readLiveBlocks — the diff basis) * 3. page.updateMany (the atomic CAS, first statement in tx) * 4. block.findMany (the existing-rows snapshot, for soft-delete) * 5. $executeRaw (ONE multi-row upsert for ALL blocks) * (+ block.updateMany when rows are soft-deleted — also constant). * * The counting mock records every prisma call; the assertions are: * - the total query count is IDENTICAL for 10, 100 and 1000 blocks; * - exactly ONE `$executeRaw` for any block count (the N+1 is gone); * - the count stays within the constant bound (no per-block statements). */ import { describe, expect, it } from "vitest"; import type { PrismaService } from "../../../common/prisma.service.js"; import { InternalBlocksService } from "../internal.service.js"; const PAGE_ID = "pg_qshape"; const WS_ID = "ws_qshape"; const BASE_VERSION = 41; /** A full block row (the shape `readLiveBlocks` maps). */ type Row = { id: string; parentId: string | null; type: string; position: string; content: Record; properties: Record; deletedAt: null | string; }; /** A prisma-shaped mock that COUNTS every statement (the flatness probe). */ function countingPrisma(existing: Row[] = []) { const calls: string[] = []; const executeRaw = async (strings: TemplateStringsArray, ..._values: unknown[]): Promise => { calls.push("$executeRaw"); return 0; }; const prisma = { page: { async findUnique(): Promise { calls.push("page.findUnique"); return { id: PAGE_ID, workspaceId: WS_ID, version: BASE_VERSION, deletedAt: null }; }, async updateMany(): Promise<{ count: number }> { calls.push("page.updateMany"); return { count: 1 }; } }, block: { async findMany(): Promise { calls.push("block.findMany"); return existing; }, async updateMany(): Promise<{ count: number }> { calls.push("block.updateMany"); return { count: existing.length }; } }, auditLog: { async create(): Promise { return {}; } }, async $transaction(fn: (tx: unknown) => Promise): Promise { return fn({ page: this.page, block: this.block, $executeRaw: async (s: TemplateStringsArray, ...v: unknown[]) => executeRaw(s, ...v) }); }, $executeRaw: executeRaw, async $connect(): Promise {}, async $disconnect(): Promise {} }; return { prisma, calls }; } function makeService(prisma: unknown) { return new InternalBlocksService( prisma as unknown as PrismaService, { write: async (): Promise => {} } as never, { publish: async (): Promise => {} } as never ); } function makeBlocks(n: number): Array<{ id: string; type: string; position: string; content: Record; parentId?: string | null; }> { return Array.from({ length: n }, (_, i) => ({ id: `blk_${i.toString(36).padStart(8, "0")}`, type: "paragraph", position: `p${i.toString(36).padStart(8, "0")}`, content: { text: `block ${i}` }, ...(i > 0 ? { parentId: `blk_${(i - 1).toString(36).padStart(8, "0")}` } : { parentId: null }) })); } describe("flush query shape — N+1 elimination (TASK-0135)", () => { it("issues a CONSTANT number of queries across batch size 10/100/1000", async () => { const counts = new Map(); for (const n of [10, 100, 1000]) { const { prisma, calls } = countingPrisma(); const service = makeService(prisma); const result = await service.internalFlush({ pageId: PAGE_ID, baseVersion: BASE_VERSION, blocks: makeBlocks(n) }); expect(result).toEqual({ version: BASE_VERSION + 1 }); counts.set(n, calls.length); } const [c10, c100, c1000] = [counts.get(10), counts.get(100), counts.get(1000)]; // The flatness the spec asserts: the count does NOT grow with N. expect(c10).toBe(c100); expect(c100).toBe(c1000); // The constant bound: page read + live read + CAS + snapshot + one // multi-row upsert (the soft-delete branch is skipped — nothing removed). expect(c10).toBeLessThanOrEqual(5); }); it("uses exactly ONE upsert statement for the whole set (the N+1 is gone)", async () => { const { prisma, calls } = countingPrisma(); const service = makeService(prisma); await service.internalFlush({ pageId: PAGE_ID, baseVersion: BASE_VERSION, blocks: makeBlocks(1000) }); const upserts = calls.filter((c) => c === "$executeRaw").length; expect(upserts).toBe(1); }); it("the soft-delete branch also stays constant (removed rows, any size)", async () => { // The page holds 1000 live rows; the flush drops 990 of them → the // removed-list soft-delete fires (still ONE updateMany, not per-row). const existing: Row[] = Array.from({ length: 1000 }, (_, i) => ({ id: `blk_${i.toString(36).padStart(8, "0")}`, parentId: null, type: "PARAGRAPH", position: `p${i.toString(36).padStart(8, "0")}`, content: { text: `existing ${i}` }, properties: {}, deletedAt: null })); const { prisma, calls } = countingPrisma(existing); const service = makeService(prisma); await service.internalFlush({ pageId: PAGE_ID, baseVersion: BASE_VERSION, blocks: makeBlocks(10).slice(0, 10) }); const softDeletes = calls.filter((c) => c === "block.updateMany").length; expect(softDeletes).toBe(1); expect(calls.length).toBeLessThanOrEqual(6); }); });