fix(api): tient les très gros fournisseurs (132k articles)

Le journal du fournisseur D005 montre 132 388 lignes persistées pour un seul
fournisseur. À cette échelle, /api/v1/grid sans limit ne tenait pas.

Deux plantages francs, indépendants de la taille de la réponse :
- getNetworkMetricsByCodeCentrale : inArray avec 132 000 valeurs, soit autant
  de paramètres liés, alors que le protocole PostgreSQL en accepte 65535.
- pgGetGammesByCodeins : même problème sur son IN (…).
Les deux lectures sont désormais découpées par lots de 2000. Le bug existait
pour tout appel dépassant ~65 000 codes, indépendamment de l'API.

Volume : charger 132k lignes complètes puis les sérialiser d'un bloc représente
plusieurs centaines de Mo en mémoire. Sans `limit`, la réponse est maintenant
diffusée en flux, par lots internes de 1000, à mémoire constante. La forme
{ data, pagination, meta } est inchangée — un client existant ne voit que des
octets arrivant progressivement.

Une erreur survenant après les premiers octets ne peut plus changer le code
HTTP : le document est alors fermé avec meta.erreur et meta.complet=false, pour
qu'une réponse tronquée ne passe pas pour complète.

`fields` reste le levier décisif à cette échelle : une ligne complète porte les
séries mensuelles et les ventilations par magasin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
This commit is contained in:
Claude committed 2026-08-07 05:45:49 +00:00
1 parent 33b30dc8bd
commit 8fe294924b
3 files changed
+135 -26

No files matched your search

+22 -15
View File
@@ -314,22 +314,29 @@ export async function pgGetGammesByCodeins(codeins: string[]): Promise<Map<strin
const unique = [...new Set(codeins.map(c => String(c ?? "").trim()).filter(Boolean))];
if (unique.length === 0) return new Map();
// DISTINCT ON (codein) → 1 gamme par article, saison la plus récente en premier.
const result = await pgNoParallel(sql`
SELECT DISTINCT ON (a.codein)
TRIM(a.codein::text) AS codein,
g.code AS gamme_code
FROM art_gamme_saison ags
JOIN articles a ON a.no_id = ags.artnoid
JOIN gammes g ON g.no_id = ags.idgamme
JOIN saisons s ON s.no_id = ags.idsaison
WHERE TRIM(a.codein::text) IN (${sql.join(unique.map(c => sql`${c}`), sql`, `)})
ORDER BY a.codein, s.no_id DESC
`);
// Découpage obligatoire : chaque codein devient un paramètre lié, or PostgreSQL
// en accepte 65535 au maximum. Un gros fournisseur dépasse 130 000 articles —
// la requête échouait alors purement et simplement.
const CHUNK = 2000;
const map = new Map<string, string>();
for (const row of result.rows as unknown as { codein: string; gamme_code: string }[]) {
if (row.codein && row.gamme_code) map.set(row.codein, String(row.gamme_code).trim());
for (let i = 0; i < unique.length; i += CHUNK) {
const batch = unique.slice(i, i + CHUNK);
// DISTINCT ON (codein) → 1 gamme par article, saison la plus récente en premier.
const result = await pgNoParallel(sql`
SELECT DISTINCT ON (a.codein)
TRIM(a.codein::text) AS codein,
g.code AS gamme_code
FROM art_gamme_saison ags
JOIN articles a ON a.no_id = ags.artnoid
JOIN gammes g ON g.no_id = ags.idgamme
JOIN saisons s ON s.no_id = ags.idsaison
WHERE TRIM(a.codein::text) IN (${sql.join(batch.map(c => sql`${c}`), sql`, `)})
ORDER BY a.codein, s.no_id DESC
`);
for (const row of result.rows as unknown as { codein: string; gamme_code: string }[]) {
if (row.codein && row.gamme_code) map.set(row.codein, String(row.gamme_code).trim());
}
}
console.log(`[pg-ff] pgGetGammesByCodeins: ${map.size}/${unique.length} articles avec gamme`);
return map;
+13 -4
View File
@@ -42,10 +42,19 @@ export async function getNetworkMetricsByCodeCentrale(
const unique = [...new Set(codes.filter(Boolean))];
if (unique.length === 0) return out;
const rows = await db
.select()
.from(qlikNetworkMetrics)
.where(inArray(qlikNetworkMetrics.codeCentrale, unique));
// Découpage obligatoire : `inArray` produit un paramètre lié par valeur, or le
// protocole PostgreSQL en accepte 65535 au maximum. Un gros fournisseur peut
// dépasser 130 000 codes — la requête échouait alors purement et simplement.
const CHUNK = 2000;
const rows: Array<typeof qlikNetworkMetrics.$inferSelect> = [];
for (let i = 0; i < unique.length; i += CHUNK) {
const batch = unique.slice(i, i + CHUNK);
const part = await db
.select()
.from(qlikNetworkMetrics)
.where(inArray(qlikNetworkMetrics.codeCentrale, batch));
rows.push(...part);
}
for (const r of rows) {
out.set(r.codeCentrale, {