1
25import { describe, expect, it } from "vitest";
26import type { PrismaService } from "../../../common/prisma.service.js";
27import { InternalBlocksService } from "../internal.service.js";
28
29const PAGE_ID = "pg_qshape";
30const WS_ID = "ws_qshape";
31const BASE_VERSION = 41;
32
33
34type Row = {
35 id: string;
36 parentId: string | null;
37 type: string;
38 position: string;
39 content: Record<string, unknown>;
40 properties: Record<string, unknown>;
41 deletedAt: null | string;
42};
43
44
45function countingPrisma(existing: Row[] = []) {
46 const calls: string[] = [];
47 const executeRaw = async (strings: TemplateStringsArray, ..._values: unknown[]): Promise<number> => {
48 calls.push("$executeRaw");
49 return 0;
50 };
51 const prisma = {
52 page: {
53 async findUnique(): Promise<unknown> {
54 calls.push("page.findUnique");
55 return { id: PAGE_ID, workspaceId: WS_ID, version: BASE_VERSION, deletedAt: null };
56 },
57 async updateMany(): Promise<{ count: number }> {
58 calls.push("page.updateMany");
59 return { count: 1 };
60 }
61 },
62 block: {
63 async findMany(): Promise<unknown[]> {
64 calls.push("block.findMany");
65 return existing;
66 },
67 async updateMany(): Promise<{ count: number }> {
68 calls.push("block.updateMany");
69 return { count: existing.length };
70 }
71 },
72 auditLog: { async create(): Promise<unknown> { return {}; } },
73 async $transaction(fn: (tx: unknown) => Promise<unknown>): Promise<unknown> {
74 return fn({
75 page: this.page,
76 block: this.block,
77 $executeRaw: async (s: TemplateStringsArray, ...v: unknown[]) => executeRaw(s, ...v)
78 });
79 },
80 $executeRaw: executeRaw,
81 async $connect(): Promise<void> {},
82 async $disconnect(): Promise<void> {}
83 };
84 return { prisma, calls };
85}
86
87function makeService(prisma: unknown) {
88 return new InternalBlocksService(
89 prisma as unknown as PrismaService,
90 { write: async (): Promise<void> => {} } as never,
91 { publish: async (): Promise<void> => {} } as never
92 );
93}
94
95function makeBlocks(n: number): Array<{
96 id: string;
97 type: string;
98 position: string;
99 content: Record<string, unknown>;
100 parentId?: string | null;
101}> {
102 return Array.from({ length: n }, (_, i) => ({
103 id: `blk_${i.toString(36).padStart(8, "0")}`,
104 type: "paragraph",
105 position: `p${i.toString(36).padStart(8, "0")}`,
106 content: { text: `block ${i}` },
107 ...(i > 0 ? { parentId: `blk_${(i - 1).toString(36).padStart(8, "0")}` } : { parentId: null })
108 }));
109}
110
111describe("flush query shape — N+1 elimination (TASK-0135)", () => {
112 it("issues a CONSTANT number of queries across batch size 10/100/1000", async () => {
113 const counts = new Map<number, number>();
114 for (const n of [10, 100, 1000]) {
115 const { prisma, calls } = countingPrisma();
116 const service = makeService(prisma);
117 const result = await service.internalFlush({
118 pageId: PAGE_ID,
119 baseVersion: BASE_VERSION,
120 blocks: makeBlocks(n)
121 });
122 expect(result).toEqual({ version: BASE_VERSION + 1 });
123 counts.set(n, calls.length);
124 }
125 const [c10, c100, c1000] = [counts.get(10), counts.get(100), counts.get(1000)];
126
127 expect(c10).toBe(c100);
128 expect(c100).toBe(c1000);
129
130
131 expect(c10).toBeLessThanOrEqual(5);
132 });
133
134 it("uses exactly ONE upsert statement for the whole set (the N+1 is gone)", async () => {
135 const { prisma, calls } = countingPrisma();
136 const service = makeService(prisma);
137 await service.internalFlush({
138 pageId: PAGE_ID,
139 baseVersion: BASE_VERSION,
140 blocks: makeBlocks(1000)
141 });
142 const upserts = calls.filter((c) => c === "$executeRaw").length;
143 expect(upserts).toBe(1);
144 });
145
146 it("the soft-delete branch also stays constant (removed rows, any size)", async () => {
147
148
149 const existing: Row[] = Array.from({ length: 1000 }, (_, i) => ({
150 id: `blk_${i.toString(36).padStart(8, "0")}`,
151 parentId: null,
152 type: "PARAGRAPH",
153 position: `p${i.toString(36).padStart(8, "0")}`,
154 content: { text: `existing ${i}` },
155 properties: {},
156 deletedAt: null
157 }));
158 const { prisma, calls } = countingPrisma(existing);
159 const service = makeService(prisma);
160 await service.internalFlush({
161 pageId: PAGE_ID,
162 baseVersion: BASE_VERSION,
163 blocks: makeBlocks(10).slice(0, 10)
164 });
165 const softDeletes = calls.filter((c) => c === "block.updateMany").length;
166 expect(softDeletes).toBe(1);
167 expect(calls.length).toBeLessThanOrEqual(6);
168 });
169});
170