From c10e8254f56adec9aa7d7878bcd99f88fae67247 Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Wed, 24 Jun 2026 18:47:08 +0530 Subject: [PATCH] fix(fumadb): mirror aggregate review hardening --- .../core/fumadb/src/adapters/drizzle/query.ts | 1 + .../core/fumadb/src/query/aggregate.test.ts | 18 +++++++++++++ packages/core/fumadb/src/query/aggregate.ts | 15 ++++++++++- packages/core/sdk/src/executor.ts | 25 ++++++++++++------- packages/core/sdk/src/plugin-storage.ts | 8 +++++- 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/packages/core/fumadb/src/adapters/drizzle/query.ts b/packages/core/fumadb/src/adapters/drizzle/query.ts index 96441b420..e9961c7c3 100644 --- a/packages/core/fumadb/src/adapters/drizzle/query.ts +++ b/packages/core/fumadb/src/adapters/drizzle/query.ts @@ -362,6 +362,7 @@ export function fromDrizzle( ); } if (filter.kind === "or") { + if (filter.items.length === 0) return Drizzle.sql`1 = 0`; return Drizzle.or( ...filter.items.map((item) => buildJsonFilter(jsonColumn, item)), ); diff --git a/packages/core/fumadb/src/query/aggregate.test.ts b/packages/core/fumadb/src/query/aggregate.test.ts index 45abe5332..00793e79a 100644 --- a/packages/core/fumadb/src/query/aggregate.test.ts +++ b/packages/core/fumadb/src/query/aggregate.test.ts @@ -128,6 +128,24 @@ const runSuite = (name: string, makeHarness: () => Promise) => { ).toBe(2); }); + it("matches empty composite filter semantics", async () => { + const { orm } = harness; + expect( + await orm.jsonCount("events", { + column: "data", + where: inT1, + filter: { kind: "or", items: [] }, + }), + ).toBe(0); + expect( + await orm.jsonCount("events", { + column: "data", + where: inT1, + filter: { kind: "and", items: [] }, + }), + ).toBe(4); + }); + it("jsonGroupCount counts distinct values", async () => { const rows = await harness.orm.jsonGroupCount("events", { column: "data", diff --git a/packages/core/fumadb/src/query/aggregate.ts b/packages/core/fumadb/src/query/aggregate.ts index 6d6e3fcbf..5f3075440 100644 --- a/packages/core/fumadb/src/query/aggregate.ts +++ b/packages/core/fumadb/src/query/aggregate.ts @@ -91,7 +91,13 @@ export interface JsonTimeBucketRow { export interface JsonStatsAdapterOptions extends JsonAdapterBase { /** Numeric path the stats are computed over. */ readonly path: JsonPath; - /** Percentile fractions in [0, 1] (e.g. 0.5, 0.95). */ + /** + * Percentile fractions in [0, 1] (e.g. 0.5, 0.95). + * + * SQLite adapters compute percentiles from all matching numeric values after + * projection because SQLite has no native percentile aggregate. Count, min, + * and max still run as SQL aggregates. + */ readonly percentiles?: readonly number[]; } @@ -155,6 +161,13 @@ export interface JsonTimeBucketOptions> extends JsonPublicBase { readonly path: JsonPath; + /** + * Percentile fractions in [0, 1] (e.g. 0.5, 0.95). + * + * SQLite adapters compute percentiles from all matching numeric values after + * projection because SQLite has no native percentile aggregate. Count, min, + * and max still run as SQL aggregates. + */ readonly percentiles?: readonly number[]; } diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts index 3ab3757dc..befee8d87 100644 --- a/packages/core/sdk/src/executor.ts +++ b/packages/core/sdk/src/executor.ts @@ -7,6 +7,7 @@ import { withQueryContext, type Condition, type ConditionBuilder, + type JsonCompareOperator, type JsonFilter, type JsonGroupCountRow, type JsonKeysetCursor, @@ -1115,6 +1116,17 @@ const isPluginStorageRecord = (value: unknown): value is Readonly", + gte: ">=", + lt: "<", + lte: "<=", +} satisfies Record< + Exclude<(typeof pluginStorageWhereOperators)[number], "in">, + JsonCompareOperator +>; + const isPluginStorageWhereFilter = (value: unknown): value is Readonly> => isPluginStorageRecord(value) && pluginStorageWhereOperators.some((operator) => operator in value); @@ -1211,16 +1223,11 @@ const pluginStorageWhereToJsonFilter = ( }); continue; } + if (!(operator in pluginStorageJsonCompareOperators)) continue; const compareOperator = - operator === "gt" - ? ">" - : operator === "gte" - ? ">=" - : operator === "lt" - ? "<" - : operator === "lte" - ? "<=" - : "="; + pluginStorageJsonCompareOperators[ + operator as keyof typeof pluginStorageJsonCompareOperators + ]; items.push({ kind: "compare", path, diff --git a/packages/core/sdk/src/plugin-storage.ts b/packages/core/sdk/src/plugin-storage.ts index 7c010406b..055dcbb10 100644 --- a/packages/core/sdk/src/plugin-storage.ts +++ b/packages/core/sdk/src/plugin-storage.ts @@ -225,7 +225,13 @@ export interface PluginStorageStatsInput< > extends PluginStorageAggregateFilter { /** Numeric field the stats are computed over. */ readonly field: PluginStorageCollectionIndexedField; - /** Percentile fractions in [0, 1] (e.g. 0.5, 0.95). */ + /** + * Percentile fractions in [0, 1] (e.g. 0.5, 0.95). + * + * SQLite-backed storage computes percentiles from all matching numeric values + * after projection because SQLite has no native percentile aggregate. Count, + * min, and max still run as SQL aggregates. + */ readonly percentiles?: readonly number[]; }