History

tools: collapse annotation N+1 + push sourceId filter to DB

openapi/graphql resolveAnnotations ran one listOperationsBySource per distinct scope in the toolRows. Store the owning scopeId on each StoredOperation instead and issue a single query, partitioning by op.scopeId so shadowed tools still resolve against the right binding. Also push executor.tools.list's sourceId filter down to storage so `/sources/:id/tools` stops pulling every row in the scope stack.

Rhys Sullivan committed 6 months ago

Commit 400a8061 parent aa220d75 files · +44 −40Browse files

Unified · Split

5 files
  1. packages/core/sdk/src/executor.ts +10 −1
  2. packages/plugins/graphql/src/sdk/plugin.ts +13 −15
  3. packages/plugins/graphql/src/sdk/store.ts +4 −6
  4. packages/plugins/openapi/src/sdk/plugin.ts +13 −12
  5. packages/plugins/openapi/src/sdk/store.ts +4 −6
packages/core/sdk/src/executor.ts+10−1
@@ -1762,7 +1762,16 @@ export const createExecutor = < · 1761 unmodified lines
17621762
17631763 const listTools = (filter?: ToolListFilter) =>
17641764 Effect.gen(function* () {
1765− const dynamic = yield* core.findMany({ model: "tool" });
1765+ // Push sourceId down to storage so we don't pull every tool row
1766+ // in the stack when the caller only wants one source (e.g. the
1767+ // `/sources/:id/tools` endpoint). `query` is a free-text match
1768+ // that still needs the row contents, so we apply it in memory.
1769+ const dynamic = yield* core.findMany({
1770+ model: "tool",
1771+ where: filter?.sourceId
1772+ ? [{ field: "source_id", value: filter.sourceId }]
1773+ : undefined,
1774+ });
17661775 // Dedup by tool id, innermost scope winning — same reason as
17671776 // `listSources` above: a shadowed id must surface as one entry
17681777 // (the inner one), not two.
packages/plugins/graphql/src/sdk/plugin.ts+13−15
@@ -366,6 +366,7 @@ export const graphqlPlugin = definePlugin( · 365 unmodified lines
366366 const storedOps: StoredOperation[] = prepared.map((p) => ({
367367 toolId: `${namespace}.${p.toolPath}`,
368368 sourceId: namespace,
369+ scopeId: config.scope,
369370 binding: p.binding,
370371 }));
371372
@@ -527,22 +528,19 @@ export const graphqlPlugin = definePlugin( · 155 unmodified lines
527528 // toolRows for a single (plugin_id, source_id) group can still
528529 // straddle multiple scopes when the source is shadowed (e.g. an
529530 // org-level GraphQL source plus a per-user override that
530− // re-registers the same tool ids). Run one listOperationsBySource
531− // per distinct scope so each lookup pins {source_id, scope_id}
532− // and we don't fall through to the wrong scope's bindings.
533− const scopes = new Set<string>();
534− for (const row of toolRows as readonly ToolRow[]) {
535− scopes.add(row.scope_id as string);
536− }
531+ // re-registers the same tool ids). One query returns every op
532+ // row in the caller's stack (the scoped adapter pins
533+ // `scope_id IN (stack)`); partition by `op.scopeId` so each
534+ // tool row resolves against its own scope's binding.
535+ const ops = yield* ctx.storage.listOperationsBySource(sourceId);
537536 const byScope = new Map<string, Map<string, OperationBinding>>();
538− for (const scope of scopes) {
539− const ops = yield* ctx.storage.listOperationsBySource(
540− sourceId,
541− scope,
542− );
543− const byId = new Map<string, OperationBinding>();
544− for (const op of ops) byId.set(op.toolId, op.binding);
545− byScope.set(scope, byId);
537+ for (const op of ops) {
538+ let byId = byScope.get(op.scopeId);
539+ if (!byId) {
540+ byId = new Map<string, OperationBinding>();
541+ byScope.set(op.scopeId, byId);
542+ }
543+ byId.set(op.toolId, op.binding);
546544 }
547545
548546 const out: Record<string, ToolAnnotations> = {};
packages/plugins/graphql/src/sdk/store.ts+4−6
@@ -50,6 +50,7 @@ export interface StoredGraphqlSource { · 49 unmodified lines
5050 export interface StoredOperation {
5151 readonly toolId: string;
5252 readonly sourceId: string;
53+ readonly scopeId: string;
5354 readonly binding: OperationBinding;
5455 }
5556
@@ -126,7 +127,6 @@ export interface GraphqlStore { · 70 unmodified lines
126127
127128 readonly listOperationsBySource: (
128129 sourceId: string,
129− scope: string,
130130 ) => Effect.Effect<readonly StoredOperation[], StorageFailure>;
131131
132132 readonly removeSource: (
@@ -153,6 +153,7 @@ export const makeDefaultGraphqlStore = ({ · 20 unmodified lines
153153 const rowToOperation = (row: Record<string, unknown>): StoredOperation => ({
154154 toolId: row.id as string,
155155 sourceId: row.source_id as string,
156+ scopeId: row.scope_id as string,
156157 binding: decodeBinding(row.binding),
157158 });
158159
@@ -257,14 +258,11 @@ export const makeDefaultGraphqlStore = ({ · 98 unmodified lines
257258 })
258259 .pipe(Effect.map((row) => (row ? rowToOperation(row) : null))),
259260
260− listOperationsBySource: (sourceId, scope) =>
261+ listOperationsBySource: (sourceId) =>
261262 db
262263 .findMany({
263264 model: "graphql_operation",
264− where: [
265− { field: "source_id", value: sourceId },
266− { field: "scope_id", value: scope },
267− ],
265+ where: [{ field: "source_id", value: sourceId }],
268266 })
269267 .pipe(Effect.map((rows) => rows.map(rowToOperation))),
270268
packages/plugins/openapi/src/sdk/plugin.ts+13−12
@@ -408,6 +408,7 @@ export const openApiPlugin = definePlugin( · 407 unmodified lines
408408 const storedOps: StoredOperation[] = definitions.map((def) => ({
409409 toolId: `${namespace}.${def.toolPath}`,
410410 sourceId: namespace,
411+ scopeId: input.scope,
411412 binding: toBinding(def),
412413 }));
413414
@@ -949,19 +950,19 @@ export const openApiPlugin = definePlugin( · 535 unmodified lines
949950 // toolRows for a single (plugin_id, source_id) group can still
950951 // straddle multiple scopes when the source is shadowed (e.g. an
951952 // org-level openapi source plus a per-user override that
952− // re-registers the same tool ids). Run one listOperationsBySource
953− // per distinct scope so each lookup pins {source_id, scope_id}
954− // and we don't fall through to the wrong scope's bindings.
955− const scopes = new Set<string>();
956− for (const row of toolRows as readonly ToolRow[]) {
957− scopes.add(row.scope_id as string);
958− }
953+ // re-registers the same tool ids). One query returns every op
954+ // row in the caller's stack (the scoped adapter pins
955+ // `scope_id IN (stack)`); partition by `op.scopeId` so each
956+ // tool row resolves against its own scope's binding.
957+ const ops = yield* ctx.storage.listOperationsBySource(sourceId);
959958 const byScope = new Map<string, Map<string, OperationBinding>>();
960− for (const scope of scopes) {
961− const ops = yield* ctx.storage.listOperationsBySource(sourceId, scope);
962− const byId = new Map<string, OperationBinding>();
963− for (const op of ops) byId.set(op.toolId, op.binding);
964− byScope.set(scope, byId);
959+ for (const op of ops) {
960+ let byId = byScope.get(op.scopeId);
961+ if (!byId) {
962+ byId = new Map<string, OperationBinding>();
963+ byScope.set(op.scopeId, byId);
964+ }
965+ byId.set(op.toolId, op.binding);
965966 }
966967
967968 const out: Record<string, ToolAnnotations> = {};
packages/plugins/openapi/src/sdk/store.ts+4−6
@@ -119,6 +119,7 @@ export type StoredSourceSchemaType = typeof StoredSourceSchema.Type; · 118 unmodified lines
119119 export interface StoredOperation {
120120 readonly toolId: string;
121121 readonly sourceId: string;
122+ readonly scopeId: string;
122123 readonly binding: OperationBinding;
123124 }
124125
@@ -202,7 +203,6 @@ export interface OpenapiStore { · 77 unmodified lines
202203
203204 readonly listOperationsBySource: (
204205 sourceId: string,
205− scope: string,
206206 ) => Effect.Effect<readonly StoredOperation[], StorageFailure>;
207207
208208 readonly removeSource: (
@@ -255,6 +255,7 @@ export const makeDefaultOpenapiStore = ({ · 46 unmodified lines
255255 const rowToOperation = (row: Record<string, unknown>): StoredOperation => ({
256256 toolId: row.id as string,
257257 sourceId: row.source_id as string,
258+ scopeId: row.scope_id as string,
258259 binding: decodeBinding(
259260 typeof row.binding === "string" ? JSON.parse(row.binding) : row.binding,
260261 ),
@@ -388,14 +389,11 @@ export const makeDefaultOpenapiStore = ({ · 127 unmodified lines
388389 })
389390 .pipe(Effect.map((row) => (row ? rowToOperation(row) : null))),
390391
391− listOperationsBySource: (sourceId, scope) =>
392+ listOperationsBySource: (sourceId) =>
392393 adapter
393394 .findMany({
394395 model: "openapi_operation",
395− where: [
396− { field: "source_id", value: sourceId },
397− { field: "scope_id", value: scope },
398− ],
396+ where: [{ field: "source_id", value: sourceId }],
399397 })
400398 .pipe(Effect.map((rows) => rows.map(rowToOperation))),
401399