Skip to content

Commit 24ecfd5

Browse files
committed
fix(admin): materialise segment CASE via subquery in getMultiYearGapTrend
The previous workforce/NAF query inlined the CASE expression directly in SELECT, GROUP BY and ORDER BY. Drizzle re-binds fresh parameter placeholders (`$1..$6`, `$10..$15`, `$16..$21`) at each call site, so Postgres sees three textually different expressions and rejects the query with 'column must appear in the GROUP BY clause'. Push the CASE into an inner subquery aliased `gap_trend_src.segment`, then aggregate on the alias in the outer query. Parameters are bound exactly once, the outer GROUP BY / ORDER BY only references column names, and the Postgres planner is happy. No observable behaviour change for consumers. Fixes the 500 the user was hitting in local dev when selecting 'Segmenter par NAF'.
1 parent 0ea140e commit 24ecfd5

2 files changed

Lines changed: 31 additions & 11 deletions

File tree

packages/app/src/server/api/routers/__tests__/adminStats.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,14 @@ type TrendRow = {
306306
};
307307

308308
/**
309-
* Build a DB mock matching the getMultiYearGapTrend chain:
310-
* select → from → optional innerJoin → where → groupBy → orderBy (resolves).
309+
* Build a DB mock matching the getMultiYearGapTrend chain. The workforce /
310+
* NAF branch runs two queries: an inner `select → from → innerJoin → where
311+
* → as()` that materialises the CASE-derived segment column, then an outer
312+
* `select → from → groupBy → orderBy (resolves)` that aggregates on the
313+
* subquery alias. The global branch only runs the final aggregating chain
314+
* (no innerJoin when there are no filters). A single chained mock covers
315+
* both shapes — every method returns `this` so the graph can be walked in
316+
* any order before `orderBy` resolves with the seeded rows.
311317
*/
312318
function buildTrendDb(rows: TrendRow[]) {
313319
const orderBy = vi.fn().mockResolvedValue(rows);
@@ -317,6 +323,7 @@ function buildTrendDb(rows: TrendRow[]) {
317323
where: vi.fn().mockReturnThis() as ReturnType<typeof vi.fn>,
318324
groupBy: vi.fn().mockReturnThis() as ReturnType<typeof vi.fn>,
319325
orderBy,
326+
as: vi.fn().mockReturnThis() as ReturnType<typeof vi.fn>,
320327
};
321328
return {
322329
select: vi.fn().mockReturnValue(chain),

packages/app/src/server/api/routers/adminStats.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -237,22 +237,35 @@ export const adminStatsRouter = createTRPCRouter({
237237
];
238238
}
239239

240-
// Workforce / NAF modes: the segment column is a CASE expression
241-
// (a real expression, not a constant), so Postgres is happy to see
242-
// it in both SELECT and GROUP BY.
240+
// Workforce / NAF modes: the segment column is a CASE expression.
241+
// We *must* compute it once in an inner subquery and reference the
242+
// alias from the outer aggregation. Inlining the CASE in SELECT +
243+
// GROUP BY + ORDER BY directly makes Drizzle re-bind fresh `$N`
244+
// parameters at each call site — Postgres then sees three textually
245+
// different CASE expressions and rejects the GROUP BY with
246+
// "column must appear in the GROUP BY clause".
243247
const segmentExpr = buildSegmentExpression(input.segmentBy);
244-
const rows = await ctx.db
248+
const innerSub = ctx.db
245249
.select({
246250
year: declarations.year,
247-
segment: segmentExpr,
248-
avgGap: sql<string | null>`avg(${declarations.averageGap})`,
249-
sampleSize: sql<number>`count(*)::int`,
251+
segment: segmentExpr.as("segment"),
252+
averageGap: declarations.averageGap,
250253
})
251254
.from(declarations)
252255
.innerJoin(companies, eq(declarations.siren, companies.siren))
253256
.where(and(...filters))
254-
.groupBy(declarations.year, segmentExpr)
255-
.orderBy(declarations.year, segmentExpr);
257+
.as("gap_trend_src");
258+
259+
const rows = await ctx.db
260+
.select({
261+
year: innerSub.year,
262+
segment: innerSub.segment,
263+
avgGap: sql<string | null>`avg(${innerSub.averageGap})`,
264+
sampleSize: sql<number>`count(*)::int`,
265+
})
266+
.from(innerSub)
267+
.groupBy(innerSub.year, innerSub.segment)
268+
.orderBy(innerSub.year, innerSub.segment);
256269

257270
return buildTrendSeries(rows);
258271
}),

0 commit comments

Comments
 (0)