History

Merge pull request #275 from RhysSullivan/rs/storage-bulk-insert

fix(storage): require createMany on CustomAdapter; drizzle issues chunked bulk INSERT

Rhys Sullivan committed 6 months ago

Commit 370df072 parents bee25da 93ff4e64 files · +72 −7Browse files

Unified · Split

4 files
  1. packages/core/storage-core/src/adapter.ts +12 −0
  2. packages/core/storage-core/src/factory.ts +29 −7
  3. packages/core/storage-core/src/testing/memory.ts +7 −0
  4. packages/core/storage-drizzle/src/adapter.ts +24 −0
packages/core/storage-core/src/adapter.ts+12−0
@@ -207,6 +207,18 @@ export interface CustomAdapter { · 206 unmodified lines
207207 select?: string[] | undefined;
208208 }) => Effect.Effect<T, Error>;
209209
210+ /**
211+ * Native bulk insert. Required because over a real network (e.g.
212+ * Hyperdrive), per-row `create` inflates to N round-trips and can't
213+ * finish inside a request budget for specs with ~1000+ rows. SQL
214+ * backends issue a single multi-row INSERT; in-memory backends just
215+ * push the rows.
216+ */
217+ createMany: <T extends Record<string, unknown>>(data: {
218+ model: string;
219+ data: ReadonlyArray<T>;
220+ }) => Effect.Effect<T[], Error>;
221+
210222 update: <T>(data: {
211223 model: string;
212224 where: CleanedWhere[];
packages/core/storage-core/src/factory.ts+29−7
@@ -637,14 +637,36 @@ export const createAdapter = ( · 636 unmodified lines
637637 forceAllowId?: boolean | undefined;
638638 }) =>
639639 Effect.gen(function* () {
640− const out: R[] = [];
640+ // Delegates straight to the backend's native bulk insert — no
641+ // per-row fallback, because over a real network connection
642+ // (Hyperdrive, etc.) N round-trips would blow the request
643+ // budget for specs with thousands of operations. Transforms
644+ // still run per-row so JSON / dates / booleans serialize the
645+ // same as single `create`.
646+ const inputs: Record<string, unknown>[] = [];
641647 for (const row of data.data) {
642− const created = yield* self.create<T, R>({
643− model: data.model,
644− data: row,
645− forceAllowId: data.forceAllowId,
646− });
647− out.push(created);
648+ inputs.push(
649+ yield* maybeTransformInput(
650+ data.model,
651+ row as Record<string, unknown>,
652+ "create",
653+ data.forceAllowId === true,
654+ ),
655+ );
656+ }
657+ const res = yield* inner.createMany({
658+ model: getModelName(data.model),
659+ data: inputs,
660+ });
661+ const out: R[] = [];
662+ for (const row of res) {
663+ out.push(
664+ (yield* maybeTransformOutput(
665+ data.model,
666+ row as Record<string, unknown>,
667+ undefined,
668+ )) as unknown as R,
669+ );
648670 }
649671 return out as unknown as readonly R[];
650672 }),
packages/core/storage-core/src/testing/memory.ts+7−0
@@ -168,6 +168,13 @@ export const makeMemoryAdapter = ( · 167 unmodified lines
168168 return data;
169169 }),
170170
171+ createMany: ({ model, data }) =>
172+ Effect.sync(() => {
173+ const table = tableFor(model);
174+ for (const row of data) table.push(row as Row);
175+ return data.slice() as never;
176+ }),
177+
171178 findOne: ({ model, where, join }) =>
172179 Effect.sync(() => {
173180 const rows = filterWhere(tableFor(model), where);
packages/core/storage-drizzle/src/adapter.ts+24−0
@@ -312,6 +312,30 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 311 unmodified lines
312312 return row as never;
313313 }),
314314
315+ // Real multi-row INSERT in fixed-size chunks. One statement per
316+ // chunk, not one per row — per-row loops blow the Hyperdrive
317+ // request budget on specs with thousands of operations. Chunking
318+ // (vs a single giant statement) also keeps payload size bounded:
319+ // JSON columns like tool schemas / operation bindings can be a
320+ // few KB each, so a 2700-row insert becomes a >10MB statement
321+ // otherwise, which chokes both Hyperdrive ingress and WASM
322+ // Postgres (PGlite) in the test harness.
323+ createMany: ({ model, data }) =>
324+ Effect.gen(function* () {
325+ if (data.length === 0) return [] as never;
326+ const table = getTable(model);
327+ const CHUNK = 500;
328+ const all: Record<string, unknown>[] = [];
329+ for (let i = 0; i < data.length; i += CHUNK) {
330+ const slice = data.slice(i, i + CHUNK) as Record<string, unknown>[];
331+ const rows = (yield* runPromise("insert many returning", () =>
332+ db.insert(table).values(slice).returning(),
333+ )) as Record<string, unknown>[];
334+ for (const row of rows) all.push(row);
335+ }
336+ return all as never;
337+ }),
338+
315339 findOne: ({ model, where, join }) =>
316340 Effect.gen(function* () {
317341 const table = getTable(model);