Skip to content

Commit 370df07

Browse files
authored
Merge pull request #275 from RhysSullivan/rs/storage-bulk-insert
fix(storage): require createMany on CustomAdapter; drizzle issues chunked bulk INSERT
2 parents bee25da + 93ff4e6 commit 370df07

4 files changed

Lines changed: 72 additions & 7 deletions

File tree

‎packages/core/storage-core/src/adapter.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ export interface CustomAdapter {
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‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -637,14 +637,36 @@ export const createAdapter = (
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‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ export const makeMemoryAdapter = (
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‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,30 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => {
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);

0 commit comments

Comments
 (0)