feat(fumadb): add aggregate and keyset query support#1119
Conversation
Add reusable JSON-document aggregate and keyset pagination primitives across FumaDB memory and Drizzle adapters. Expose the pushdown through plugin storage aggregate and queryKeyset facades with focused coverage for SQLite parity, null handling, path escaping, policy scoping, and unsupported adapters.
Greptile SummaryThis PR adds SQL-pushed JSON-document aggregate primitives and keyset pagination to FumaDB, exposing them through the plugin storage SDK facade. All previous review concerns (empty
Confidence Score: 5/5Safe to merge — all five new query operations apply table read policies before reaching the adapter, and the empty-or / explicit-operator issues from the previous review are resolved. The implementation is correct and well-tested across memory, SQLite, and policy harnesses. The null-aware keyset cursor logic is exercised end-to-end (including the nullable-sort-column truncation regression). JSON path quoting, LIKE escaping, and empty composite filter semantics all have explicit parity tests between the memory and Drizzle adapters. No files require special attention. The most complex file (drizzle/query.ts) is fully covered by the aggregate.test.ts harness for SQLite, and the Postgres-specific paths (percentile_cont, nulls last/first) are intentionally deferred to a future Postgres test harness as noted in the PR description. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Plugin as Plugin Code
participant Facade as PluginStorageFacade
participant CoreDb as CoreDb
participant ORM as toORM (orm/index.ts)
participant Adapter as Drizzle/Memory Adapter
Plugin->>Facade: "storage.runs.aggregate.groupCount({ field, where })"
Facade->>Facade: validate indexed fields
Facade->>Facade: pluginStorageWhereToJsonFilter(where)
Facade->>CoreDb: "jsonGroupCount(plugin_storage, { column, where, filter })"
CoreDb->>ORM: jsonGroupCount(name, options)
ORM->>ORM: requireJsonOp + compileScopedWhere
ORM->>ORM: applyReadPolicies
ORM->>Adapter: "jsonGroupCount(table, { column, where, filter, path })"
Adapter-->>ORM: JsonGroupCountRow[]
ORM-->>Facade: JsonGroupCountRow[]
Facade-->>Plugin: PluginStorageGroupCount[]
Plugin->>Facade: "storage.runs.queryKeyset({ orderBy, cursor, limit })"
Facade->>Facade: validate indexed fields + limit
Facade->>CoreDb: "jsonPage(plugin_storage, { orderBy, keyColumn, cursor, limit })"
CoreDb->>ORM: jsonPage(name, options)
ORM->>ORM: requireJsonOp + compileScopedWhere
ORM->>ORM: applyReadPolicies
ORM->>Adapter: "jsonPage(table, { orderBy, keyColumn, keyDirection, cursor, limit })"
Note over Adapter: null-aware cursor predicate OR-terms
Adapter-->>ORM: Row[]
ORM-->>Facade: Row[]
Facade->>Facade: build nextCursor from last entry
Facade-->>Plugin: "{ entries, nextCursor }"
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Plugin as Plugin Code
participant Facade as PluginStorageFacade
participant CoreDb as CoreDb
participant ORM as toORM (orm/index.ts)
participant Adapter as Drizzle/Memory Adapter
Plugin->>Facade: "storage.runs.aggregate.groupCount({ field, where })"
Facade->>Facade: validate indexed fields
Facade->>Facade: pluginStorageWhereToJsonFilter(where)
Facade->>CoreDb: "jsonGroupCount(plugin_storage, { column, where, filter })"
CoreDb->>ORM: jsonGroupCount(name, options)
ORM->>ORM: requireJsonOp + compileScopedWhere
ORM->>ORM: applyReadPolicies
ORM->>Adapter: "jsonGroupCount(table, { column, where, filter, path })"
Adapter-->>ORM: JsonGroupCountRow[]
ORM-->>Facade: JsonGroupCountRow[]
Facade-->>Plugin: PluginStorageGroupCount[]
Plugin->>Facade: "storage.runs.queryKeyset({ orderBy, cursor, limit })"
Facade->>Facade: validate indexed fields + limit
Facade->>CoreDb: "jsonPage(plugin_storage, { orderBy, keyColumn, cursor, limit })"
CoreDb->>ORM: jsonPage(name, options)
ORM->>ORM: requireJsonOp + compileScopedWhere
ORM->>ORM: applyReadPolicies
ORM->>Adapter: "jsonPage(table, { orderBy, keyColumn, keyDirection, cursor, limit })"
Note over Adapter: null-aware cursor predicate OR-terms
Adapter-->>ORM: Row[]
ORM-->>Facade: Row[]
Facade->>Facade: build nextCursor from last entry
Facade-->>Plugin: "{ entries, nextCursor }"
Reviews (3): Last reviewed commit: "fix(fumadb): preserve empty or filter se..." | Re-trigger Greptile |
|
Additional downstream context from my fork: the FumaDB aggregate/keyset primitive is what lets an execution-history read model stay queryable without pulling whole plugin-storage collections into JS. High-level shape: AST outline of downstream usage: executionHistoryStore.list(options)
-> computeMeta(options)
-> aggregate.count(...)
-> aggregate.groupCount(...)
-> aggregate.stats(...)
-> aggregate.timeBuckets(...)
-> runsC.queryKeyset(...)
-> return { runs, nextCursor, meta }Fork permalinks:
Call stack: This PR stays focused on reusable query capability. If it lands, execution history, metrics dashboards, and other plugin-owned read models can build richer list and facet APIs without adding bespoke SQL per plugin. |
## Summary Mirrors the review-hardening deltas from upstream [RhysSullivan#1119](RhysSullivan#1119) onto `dev`. `dev` already contains the broader FumaDB aggregate and keyset query feature, so this PR only carries the remaining drift from the upstream review loop. ## Changes - Preserve memory and Drizzle parity for empty composite filters by compiling empty JSON `or` filters to a constant false SQL predicate. - Add regression coverage for empty `or` and empty `and` filters across the aggregate harness. - Replace nested plugin-storage operator selection with an explicit typed JSON compare-operator map. - Document SQLite percentile behavior on the public FumaDB and plugin-storage stats inputs. ## Intentional Differences From Upstream RhysSullivan#1119 - This PR does not re-add the full aggregate and keyset implementation because `dev` already has that feature surface. - This PR does not touch the OpenAPI storage facade mock because `dev` already has the mock shape needed by the expanded collection facade. - This PR has no changeset because it only mirrors fixes to an existing `dev` feature surface. ## Tests - `bun run bootstrap` - `bun run --cwd packages/core/fumadb test -- src/query/aggregate.test.ts src/query/table-policy.test.ts` - `bun run --cwd packages/core/sdk test -- src/plugin-storage-aggregate.test.ts src/plugin-storage.test.ts` - `bun run --cwd packages/core/fumadb typecheck` - `bun run --cwd packages/core/sdk typecheck` - `bun run typecheck` - `./node_modules/.bin/oxfmt --check packages/core/fumadb/src/adapters/drizzle/query.ts packages/core/fumadb/src/query/aggregate.test.ts packages/core/fumadb/src/query/aggregate.ts packages/core/sdk/src/executor.ts packages/core/sdk/src/plugin-storage.ts`
Summary
collection.aggregate.{count,groupCount,timeBuckets,stats}andcollection.queryKeyset(...).Changes
AbstractQuerymethods:jsonCount,jsonGroupCount,jsonTimeBuckets,jsonStats, andjsonPage.ORMAdapterhooks andtoORMforwarding that applies table read policies before adapter calls and fails loudly for unsupported adapters.@executor-js/fumadband@executor-js/sdksurface.AST-level Outline
Call-stack Trace
Direct FumaDB query
Plugin storage aggregate
Keyset page
Usage Pseudocode
Tests
bun run bootstrapbun run --cwd packages/core/fumadb test -- src/query/aggregate.test.ts src/query/table-policy.test.tsbun run --cwd packages/core/sdk test -- src/plugin-storage-aggregate.test.ts src/plugin-storage.test.tsbun run --cwd packages/core/fumadb typecheckbun run --cwd packages/core/sdk typecheckbunx oxfmt --check .changeset/fumadb-json-pushdown.md packages/core/fumadb/src/adapters/drizzle/query.ts packages/core/fumadb/src/adapters/memory/index.ts packages/core/fumadb/src/query/aggregate-eval.ts packages/core/fumadb/src/query/aggregate.test.ts packages/core/fumadb/src/query/aggregate.ts packages/core/fumadb/src/query/index.ts packages/core/fumadb/src/query/orm/index.ts packages/core/fumadb/src/query/table-policy.test.ts packages/core/sdk/src/executor.ts packages/core/sdk/src/fuma-runtime.ts packages/core/sdk/src/plugin-storage-aggregate.test.ts packages/core/sdk/src/plugin-storage.ts packages/core/sdk/src/test-config.tsbunx oxlint -c .oxlintrc.jsonc --deny-warnings .changeset/fumadb-json-pushdown.md packages/core/fumadb/src/adapters/drizzle/query.ts packages/core/fumadb/src/adapters/memory/index.ts packages/core/fumadb/src/query/aggregate-eval.ts packages/core/fumadb/src/query/aggregate.test.ts packages/core/fumadb/src/query/aggregate.ts packages/core/fumadb/src/query/index.ts packages/core/fumadb/src/query/orm/index.ts packages/core/fumadb/src/query/table-policy.test.ts packages/core/sdk/src/executor.ts packages/core/sdk/src/fuma-runtime.ts packages/core/sdk/src/plugin-storage-aggregate.test.ts packages/core/sdk/src/plugin-storage.ts packages/core/sdk/src/test-config.tsbun run lint:changelog-stubsgit diff HEAD --checkRoot
bun run format:checkandbun run lintcurrently fail on preexisting.scratchpadfiles outside this PR. The touched-file format and lint checks above pass.Deferred Scope
This PR intentionally does not include execution-history UI, runs API, semantic search, tool manifest, cache primitive work, or bulk plugin-storage operations.