Skip to content

Commit dd959ea

Browse files
committed
fix(admin): split getMultiYearGapTrend global mode to avoid GROUP BY literal
Postgres rejects string constants in `GROUP BY` ("non-integer constant in GROUP BY"). The previous implementation pushed `'Global'` as a literal segment expression through the shared code path, which made the query fail at runtime even though the Drizzle-mocked unit tests passed. Split the global mode into its own query branch: group by year only, attach the hardcoded `'Global'` series label in TypeScript. Workforce and NAF modes keep the CASE-based segment expression — those are real expressions, which Postgres happily accepts in GROUP BY. No behaviour change for consumers; fixes the empty chart observed in K10 E2E tests on CI (`<figure>` never rendered because the procedure threw 500s that the client rendered as "Aucune donnée").
1 parent 8d7e6f9 commit dd959ea

1 file changed

Lines changed: 49 additions & 26 deletions

File tree

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

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -198,27 +198,55 @@ export const adminStatsRouter = createTRPCRouter({
198198
filters.push(isNotNull(companies.nafCode));
199199
}
200200

201-
const segmentExpr = buildSegmentExpression(input.segmentBy);
202-
203-
const needsCompaniesJoin =
204-
input.segmentBy !== "global" ||
205-
shouldApplySizeRange ||
206-
shouldApplyNafFilter;
207-
208-
const baseQuery = ctx.db.select({
209-
year: declarations.year,
210-
segment: segmentExpr,
211-
avgGap: sql<string | null>`avg(${declarations.averageGap})`,
212-
sampleSize: sql<number>`count(*)::int`,
213-
});
214-
215-
const scoped = needsCompaniesJoin
216-
? baseQuery
217-
.from(declarations)
218-
.innerJoin(companies, eq(declarations.siren, companies.siren))
219-
: baseQuery.from(declarations);
201+
// Global mode collapses every row into a single series, so we group
202+
// by year only. A literal segment expression can't live in `GROUP BY`
203+
// — Postgres rejects string constants there ("non-integer constant
204+
// in GROUP BY"). The `'Global'` label is attached back in TS.
205+
if (input.segmentBy === "global") {
206+
const baseQuery = ctx.db.select({
207+
year: declarations.year,
208+
avgGap: sql<string | null>`avg(${declarations.averageGap})`,
209+
sampleSize: sql<number>`count(*)::int`,
210+
});
211+
const scoped =
212+
shouldApplySizeRange || shouldApplyNafFilter
213+
? baseQuery
214+
.from(declarations)
215+
.innerJoin(companies, eq(declarations.siren, companies.siren))
216+
: baseQuery.from(declarations);
217+
const rows = await scoped
218+
.where(and(...filters))
219+
.groupBy(declarations.year)
220+
.orderBy(declarations.year);
221+
if (rows.length === 0) return [];
222+
return [
223+
{
224+
segment: "Global",
225+
points: rows.map((row) => ({
226+
year: row.year,
227+
avgGap:
228+
row.avgGap === null
229+
? null
230+
: Math.round(Number.parseFloat(row.avgGap) * 10) / 10,
231+
sampleSize: row.sampleSize,
232+
})),
233+
},
234+
];
235+
}
220236

221-
const rows = await scoped
237+
// Workforce / NAF modes: the segment column is a CASE expression
238+
// (a real expression, not a constant), so Postgres is happy to see
239+
// it in both SELECT and GROUP BY.
240+
const segmentExpr = buildSegmentExpression(input.segmentBy);
241+
const rows = await ctx.db
242+
.select({
243+
year: declarations.year,
244+
segment: segmentExpr,
245+
avgGap: sql<string | null>`avg(${declarations.averageGap})`,
246+
sampleSize: sql<number>`count(*)::int`,
247+
})
248+
.from(declarations)
249+
.innerJoin(companies, eq(declarations.siren, companies.siren))
222250
.where(and(...filters))
223251
.groupBy(declarations.year, segmentExpr)
224252
.orderBy(declarations.year, segmentExpr);
@@ -227,12 +255,7 @@ export const adminStatsRouter = createTRPCRouter({
227255
}),
228256
});
229257

230-
function buildSegmentExpression(
231-
segmentBy: "global" | "workforce" | "naf",
232-
): SQL<string> {
233-
if (segmentBy === "global") {
234-
return sql<string>`'Global'`;
235-
}
258+
function buildSegmentExpression(segmentBy: "workforce" | "naf"): SQL<string> {
236259
if (segmentBy === "workforce") {
237260
// Mirrors the boundaries of `COMPANY_SIZE_RANGES`. Hard-coding the
238261
// buckets as literals keeps the generated SQL self-documenting; if a

0 commit comments

Comments
 (0)