Skip to content

Commit f2a006d

Browse files
committed
perf(sdk): upsert plugin storage bulk writes
1 parent aa4be4c commit f2a006d

4 files changed

Lines changed: 82 additions & 35 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@executor-js/fumadb": patch
3+
"@executor-js/sdk": patch
4+
---
5+
6+
Add a FumaDB bulk upsert query path and route plugin-storage bulk writes through
7+
it so existing rows are updated without delete/reinsert churn.

packages/core/sdk/src/executor.ts

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ import { connectionIdentifier } from "./connection-name-identifier";
143143
import { annotateToolResultOutcome } from "./tool-result";
144144

145145
const PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE = 90;
146-
const PLUGIN_STORAGE_CREATE_ROW_BATCH_SIZE = 90;
147146
const MAX_APPROVAL_ARGUMENT_PREVIEW_CHARS = 4_000;
148147

149148
// ---------------------------------------------------------------------------
@@ -596,6 +595,14 @@ type LooseStorageDb = {
596595
tableName: string,
597596
rows: readonly Record<string, unknown>[],
598597
) => Promise<readonly unknown[]>;
598+
readonly upsertMany: (
599+
tableName: string,
600+
options: {
601+
readonly target: readonly string[];
602+
readonly update: readonly string[];
603+
readonly values: readonly Record<string, unknown>[];
604+
},
605+
) => Promise<void>;
599606
readonly deleteMany: (tableName: string, options?: unknown) => Promise<void>;
600607
readonly findFirst: (
601608
tableName: string,
@@ -632,6 +639,19 @@ const makeCoreDb = (fuma: ReturnType<typeof makeFumaClient>) => ({
632639
: fuma
633640
.use(`${tableName}.createMany`, (db) => asLooseStorageDb(db).createMany(tableName, rows))
634641
.pipe(Effect.asVoid),
642+
upsertMany: <TName extends CoreTableName>(
643+
tableName: TName,
644+
options: {
645+
readonly target: readonly string[];
646+
readonly update: readonly string[];
647+
readonly values: readonly Record<string, unknown>[];
648+
},
649+
): Effect.Effect<void, StorageFailure> =>
650+
options.values.length === 0
651+
? Effect.void
652+
: fuma.use(`${tableName}.upsertMany`, (db) =>
653+
asLooseStorageDb(db).upsertMany(tableName, options),
654+
),
635655
deleteMany: <TName extends CoreTableName>(
636656
tableName: TName,
637657
options: { readonly where?: CoreWhere } = {},
@@ -981,33 +1001,22 @@ const makePluginStorageFacade = (input: {
9811001
const uniqueEntries = [...entriesById.values()];
9821002
if (uniqueEntries.length === 0) return;
9831003

984-
yield* deleteManyImpl(owner, os.subject, uniqueEntries);
985-
9861004
const now = new Date();
987-
for (
988-
let offset = 0;
989-
offset < uniqueEntries.length;
990-
offset += PLUGIN_STORAGE_CREATE_ROW_BATCH_SIZE
991-
) {
992-
const batchEntries = uniqueEntries.slice(
993-
offset,
994-
offset + PLUGIN_STORAGE_CREATE_ROW_BATCH_SIZE,
995-
);
996-
yield* input.core.createMany(
997-
"plugin_storage",
998-
batchEntries.map((entry) => ({
999-
tenant,
1000-
owner: os.owner,
1001-
subject: os.subject,
1002-
plugin_id: input.pluginId,
1003-
collection: entry.collection,
1004-
key: entry.key,
1005-
data: entry.data,
1006-
created_at: now,
1007-
updated_at: now,
1008-
})),
1009-
);
1010-
}
1005+
yield* input.core.upsertMany("plugin_storage", {
1006+
target: ["tenant", "owner", "subject", "plugin_id", "collection", "key"],
1007+
update: ["data", "updated_at"],
1008+
values: uniqueEntries.map((entry) => ({
1009+
tenant,
1010+
owner: os.owner,
1011+
subject: os.subject,
1012+
plugin_id: input.pluginId,
1013+
collection: entry.collection,
1014+
key: entry.key,
1015+
data: entry.data,
1016+
created_at: now,
1017+
updated_at: now,
1018+
})),
1019+
});
10111020
});
10121021

10131022
const removeManyImpl = (
@@ -1104,10 +1113,24 @@ const makePluginStorageFacade = (input: {
11041113
PluginStorageEntry<PluginStorageCollectionData<typeof definition>>,
11051114
StorageFailure
11061115
>,
1116+
putMany: (storageInput) =>
1117+
putManyImpl(
1118+
storageInput.owner,
1119+
storageInput.entries.map((entry) => ({
1120+
collection: definition.name,
1121+
key: entry.key,
1122+
data: entry.data,
1123+
})),
1124+
),
11071125
query: (storageInput) => queryCollection(definition, storageInput),
11081126
count: (storageInput) =>
11091127
queryCollection(definition, storageInput).pipe(Effect.map((rows) => rows.length)),
11101128
remove: (storageInput) => removeImpl(storageInput.owner, definition.name, storageInput.key),
1129+
removeMany: (storageInput) =>
1130+
removeManyImpl(
1131+
storageInput.owner,
1132+
storageInput.keys.map((key) => ({ collection: definition.name, key })),
1133+
),
11111134
}),
11121135
get: (storageInput) => getVisible(storageInput.collection, storageInput.key),
11131136
getForOwner: (storageInput) =>

packages/core/sdk/src/plugin-storage.test.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,14 @@ const executionHistoryPlugin = definePlugin(() => ({
6565
owner: Owner,
6666
rows: readonly { readonly key: string; readonly data: ToolCall }[],
6767
) =>
68-
ctx.pluginStorage.putMany({
68+
ctx.storage.toolCalls.putMany({
6969
owner,
70-
entries: rows.map((row) => ({
71-
collection: toolCalls.name,
72-
key: row.key,
73-
data: row.data,
74-
})),
70+
entries: rows,
7571
}),
7672
removeMany: (owner: Owner, keys: readonly string[]) =>
77-
ctx.pluginStorage.removeMany({
73+
ctx.storage.toolCalls.removeMany({
7874
owner,
79-
entries: keys.map((key) => ({ collection: toolCalls.name, key })),
75+
keys,
8076
}),
8177
get: (key: string) => ctx.storage.toolCalls.get({ key }),
8278
getForOwner: (owner: Owner, key: string) => ctx.storage.toolCalls.getForOwner({ owner, key }),

packages/core/sdk/src/plugin-storage.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,21 @@ export interface PluginStorageCollectionScopedKeyInput extends PluginStorageColl
135135
readonly owner: Owner;
136136
}
137137

138+
export interface PluginStorageCollectionPutManyEntry<TData extends object> {
139+
readonly key: string;
140+
readonly data: TData;
141+
}
142+
143+
export interface PluginStorageCollectionPutManyInput<TData extends object> {
144+
readonly owner: Owner;
145+
readonly entries: readonly PluginStorageCollectionPutManyEntry<TData>[];
146+
}
147+
148+
export interface PluginStorageCollectionRemoveManyInput {
149+
readonly owner: Owner;
150+
readonly keys: readonly string[];
151+
}
152+
138153
export interface PluginStorageCollectionListInput {
139154
readonly keyPrefix?: string;
140155
}
@@ -188,6 +203,9 @@ export interface PluginStorageCollectionFacade<
188203
readonly put: (
189204
input: PluginStorageCollectionPutInput<PluginStorageCollectionData<TDefinition>>,
190205
) => Effect.Effect<PluginStorageEntry<PluginStorageCollectionData<TDefinition>>, StorageFailure>;
206+
readonly putMany: (
207+
input: PluginStorageCollectionPutManyInput<PluginStorageCollectionData<TDefinition>>,
208+
) => Effect.Effect<void, StorageFailure>;
191209
readonly query: (
192210
input?: PluginStorageCollectionQueryInput<TDefinition>,
193211
) => Effect.Effect<
@@ -200,6 +218,9 @@ export interface PluginStorageCollectionFacade<
200218
readonly remove: (
201219
input: PluginStorageCollectionScopedKeyInput,
202220
) => Effect.Effect<void, StorageFailure>;
221+
readonly removeMany: (
222+
input: PluginStorageCollectionRemoveManyInput,
223+
) => Effect.Effect<void, StorageFailure>;
203224
}
204225

205226
export interface PluginStorageFacade {

0 commit comments

Comments
 (0)