france-data-mcp: Skill for Claude Code

.claude/skills/db-gotchas/SKILL.md

db-gotchas is a skill for Claude Code from cturkieh/france-data-mcp. It costs 98 tokens per session (5,915 once invoked), scanned A, original, MIT.

A collection of documented database failure cases for Supabase, PostGIS, and PostgREST. These are database and API technologies, with notes on data types, indexes, time limits, security rules, and duplicate records.

In plain words
What is it for?
Use it before changing SQL migrations, remote procedure calls, database queries, or geospatial lookups.
Why use it?
It warns about bugs that can silently produce wrong calculations, slow queries, broken swaps, or incorrect database results.

Skill for Claude Code

Written for Claude Code: installed under .claude/. Also seen: mentions CLAUDE.md; positional $N argument.

This is cturkieh/france-data-mcp's own configuration. It tells Claude Code how to work on france-data-mcp itself, so it is not a mod to install elsewhere. Copy it as a starting point and replace the rules that are about this project. Everything france-data-mcp configures →

Reuse

Borrowing it

Nothing to install: this file belongs to cturkieh/france-data-mcp. Take a copy, put it at the same path in your own repository, and replace the rules that are about this project with yours.

Copy the file
curl -O https://raw.githubusercontent.com/cturkieh/france-data-mcp/main/.claude/skills/db-gotchas/SKILL.md
Clone the repo
git clone --depth 1 https://github.com/cturkieh/france-data-mcp

Made for: Claude Code.

Wrote this? Show the measurements

A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.

agentmods badge for db-gotchas

README.md
[![agentmods](https://agentmods.dev/badge/skills/cturkieh/france-data-mcp/db-gotchas/github.svg)](https://agentmods.dev/skills/cturkieh/france-data-mcp/db-gotchas)
Your own site
<a href="https://agentmods.dev/skills/cturkieh/france-data-mcp/db-gotchas"><img src="https://agentmods.dev/badge/skills/cturkieh/france-data-mcp/db-gotchas/github.svg" alt="Measured on agentmods" height="20"></a>

Or the 80×15 button, for a site that already has a row of RSS and ATOM ones. Only the verdict fits; the numbers stay here.

agentmods 80×15 button for db-gotchas

Your own site · 80×15
<a href="https://agentmods.dev/skills/cturkieh/france-data-mcp/db-gotchas"><img src="https://agentmods.dev/badge/skills/cturkieh/france-data-mcp/db-gotchas.svg" alt="Reviewed on agentmods" width="80" height="20"></a>
Per session 98 Skills are progressive disclosure: only the name and description are preloaded; the body loads when the skill is used.
When invoked 5,915 The whole file, excluding the scripts and references it only reads on demand.
Security scan A 0 findings. A grade says what 26 rules found in the file — not that it is safe.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5.1 $0.00098 $0.05915
Opus 5 $0.00049 $0.02958
Sonnet 5 $0.00020 $0.01183
Haiku 4.5 $0.00010 $0.00592

Measured 5d ago against content hash 72f704dcba4f, method: parsed. Prices are Anthropic first-party input rates as of 2026-09-11, from the pricing page.

Security

Grade A, and why

db-gotchas scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 5d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Nothing flagged

None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.

.claude/skills/db-gotchas/SKILL.md · 30 lines

How it starts

The opening of the file, as written. The whole thing — 30 lines — stays where its author put it; the contents beside it link to each section on GitHub.

Top gotchas DB (post-mortems prouvés prod)

Déplacé verbatim depuis CLAUDE.md le 2026-09-06 (budget). Source de vérité pour ce périmètre ; CLAUDE.md ne garde que les règles de tête.

  • PostgREST sérialise NUMERIC/BIGINT en STRING JSON (préservation de précision), JAMAIS en number — toute valeur de ce type lue d'une RPC arrive en string côté supabase-js MALGRÉ un type TS number | null. Trois pièges prouvés (rpps-db.ts:322, iris-db.ts:161, iris-profil.ts) : (1) acc += row.numericField concatène (0 + "100""0100") au lieu d'additionner → agrégat faux servi en success ; (2) un champ exposé brut viole son contrat number | null (string servie au LLM, arithmétique aval cassée) ; (3) une valeur source corrompue ("N/A", secret stat "s") → Number() = NaN. RÈGLE : coercer AU BOUNDARY DB toute valeur NUMERIC/BIGINT avant arithmétique OU exposition — Number(v) + Number.isFinite ; pour une Σ, helper n(v) (null→0 neutre) ; pour un champ exposé où null est légitime, helper numOrNull(v, field) (null→null, NaN→null + console.warn grep-able — dégradation source jamais muette, cf. règle error handling). Le piège est SILENCIEUX en test si les mocks injectent des number (la prod, elle, donne des string) → tout test d'agrégation/exposition d'un NUMERIC RPC DOIT avoir un cas en string (cf. iris-profil.test.ts "réalisme PostgREST"). Alternative SQL-side (::float8 dans la RPC → PostgREST renvoie un number) écartée : romprait la précision NUMERIC et la cohérence avec la doctrine de coercition TS du projet.
  • ST_AsGeoJSON(geom)::jsonb obligatoire en sortie RPC (sinon hex EWKB).
  • Colonne calculée geog GEOGRAPHY ... STORED + index GIST (cast runtime tue le plan).
  • PostgREST proxy timeout 60s ≠ Postgres statement_timeout → batch UPDATEs par 10K.
  • LANGUAGE plpgsql + EXECUTE format(... %L::CHAR(3) ...) pour RPC filtrant une colonne CHAR(n) indexée par un param TEXT. Jamais WHERE col_char = p_text : Postgres caste la COLONNE indexée en text ((col)::text = $1) → index inutilisable → fallback seq/mauvais index (post-mortem V0.10.1, 254 ms→5,5 ms / 265 786→90 buffers). Interpoler le param en literal typé via %L::CHAR(n), garder les autres params en USING $n.
  • Matview FROM <table swappée par ingest_atomic_swap> + post-swap REFRESH-only = bombe OID (post-mortem 2026-05-18, prouvé prod). Une matview suit l'OID de sa table source, pas son nom. ingest_atomic_swap fait une rotation par RENAME (<t><t>_previous<t>_previous_OLDDROP CASCADE). Un post-swap qui se contente de REFRESH MATERIALIZED VIEW (RPC ingest_refresh_matview) ⇒ 1er cron réussi : la matview reste collée à l'ancienne table (désync SILENCIEUSE, status success, données périmées servies) ; 2e cron : DROP <t>_previous_OLD CASCADE la DÉTRUIT → tools 42P01 avalés en partial. Fix RPPS : ingest_rebuild_rpps_matviews RECONSTRUIT post-swap (DROP MV <m> + CREATE MV <m>_rebuild AS <SELECT canonique VERBATIM> FROM <t> résolu PAR NOM + index + RENAME atomique, 1 transaction PL/pgSQL ; échec transitoire→partial sans throw car rollback préserve l'ancienne, structurel→throw failed+exit). scripts/ingest/rpps-matview-rebuild.test.ts garde l'invariant + la parité DDL anti-drift. Ameli avait le MÊME défaut — CORRIGÉ 2026-05-19 par ingest_rebuild_ameli_matviews (migration 20260519T200000, réplication 1:1 du patron RPPS ci-dessus ; refreshAmeliMatviewsrebuildAmeliMatviews, garde-fou ameli-matview-rebuild.test.ts). Le DROP sans CASCADE de ameli_nomenclature_stats est prouvé prod malgré les 2 RPC LANGUAGE sql qui la référencent (fonctions sql à corps $$ = pas de dépendance catalogue bloquante ; CASCADE proscrit, droperait les RPC). Dette P1 close : shortCircuitIfSameChecksum peut désormais être optimisé sans risque. Plus AUCUN script ingest n'utilise ingest_refresh_matview (RPPS + Ameli en rebuild) — la whitelist ingest_refresh_matview reste pour toute FUTURE matview refresh-only légitime (non FROM table swappée) ; staging-parity.test.ts garde la protection whitelist générique, les invariants rebuild sont gardés par {rpps,ameli}-matview-rebuild.test.ts.
  • Parité index prod↔ingest_create_*_staging : tout index sur la table prod DOIT être mirroré dans la staging-create (sinon perdu SILENCIEUSEMENT au swap → re-régression 57014). staging-parity.test.ts garde-fou (set prod vivant = creates − drops, DROP INDEX honoré PAR NOM, regex de drop ancrée sur ; sinon une prose de commentaire « drop index X » dé-tracke un index vivant = faux négatif). Recréer ingest_create_*_staging = recopie VERBATIM de la dernière def (PostgreSQL n'a pas d'héritage de corps de fonction) ; patcher « prod − N lignes » réintroduit silencieusement un objet retiré par une migration ultérieure.
  • RPC d'ingestion longue via PostgREST = budget statement_timeout 8s hérité si la fonction n'a pas son propre SET ; + bulk COPY sans ANALYZE = plan dégradé (post-mortem 2026-05-18, RÉFUTE l'hypothèse « index BAN » ci-dessous, prouvé prod run #26046475566 + pg_roles + EXPLAIN ANALYZE). Supabase : service_role n'a AUCUN statement_timeout (rolconfig NULL) → un appel supabase-js clé SERVICE_ROLE → PostgREST hérite du statement_timeout de authenticator = 8 s (anon 3s / authenticated 8s / postgres cap 2min). Toute RPC d'ingestion longue DOIT porter un SET statement_timeout AU NIVEAU FONCTION (best practice Supabase ; valeur < 60 s = cap passerelle PostgREST, sinon timeout passerelle opaque au lieu d'un 57014 propre). De plus, ingest_apply_*_finess_enrichment_batch requête rpps_staging juste après un bulk COPY (~2,24 M lignes, table fraîchement CREATE) : sans ANALYZE le planner n'a aucune statistique → plan dégradé sur le 1er batch → >8 s → 57014 déterministe en validate, avant le swap (données intactes, cron cassé « tout seul »). Fix RPPS C : SET statement_timeout='55s' sur l'enrichment ET sur ingest_analyze_rpps_staging, cette dernière (ANALYZE rpps_staging) appelée post-COPY/pré-enrichment par rpps.ts (échec → IngestError LOUD). Garde-fou scripts/ingest/enrichment-statement-timeout.test.ts. Vérifier le budget réel : SELECT rolname, rolconfig FROM pg_roles.
  • Index fonctionnel Unicode-lourd dans ingest_create_*_staging = AGGRAVANT du run (INSERT ralenti), PAS la cause du 57014 enrichment (correctif d'un post-mortem erroné — la prod a réfuté l'inférence). Un index fonctionnel Unicode (rpps_address_key_for_index) avec prédicat partiel sur des colonnes que l'UPDATE de masse modifie alourdit la maintenance d'index pendant le COPY/UPDATE (run ~57 min) — mais le vrai déclencheur du 57014 était le budget 8 s hérité + l'absence d'ANALYZE (gotcha ci-dessus), pas cet index : son retrait seul (fix A) n'a PAS corrigé le timeout (run #26046475566). Garder néanmoins la règle d'hygiène : un index BAN/Unicode lourd doit vivre dans un step DÉDIÉ post-enrichment, JAMAIS dans ingest_create_*_staging (évite de re-rallonger le run). Leçon transverse : prouver une cause-racine par la prod avant de coder le fix ; une inférence passée en /review P1+P2 reste une inférence.
  • ban_join : pose BAN cache→staging = jumeau finess_join MAIS piloté CURSEUR KEYSET (p_after), JAMAIS sentinelle ; plus AUCUN build d'index lourd ni géocodage API dans le cron (refonte 2026-05-19, prouvée prod ; design+post-mortem consolidé docs/plans/ban-join.md ; SUPERSEDE la refonte 2026-05-18 ci-dessous). Le cache geocoded_addresses étant rempli (hors cron, par ban-backfill.mjs), il devient « une table à joindre » comme FINESS : ingest_apply_rpps_ban_join_batch(p_after, p_limit) (migration 20260519T180000, SET statement_timeout='55s', RETURNS TABLE(last_id, applied)) fait un UPDATE rpps_staging ⟕ geocoded_addresses ON g.address_key = rpps_address_key_for_index(...), lot borné WHERE id > p_after ORDER BY id LIMIT p_limit. Pourquoi keyset et NON sentinelle (prouvé prod, EXPLAIN ANALYZE transaction ROLLBACK) : la sentinelle façon FINESS re-scanne le préfixe déjà traité → quadratique → 57014 en fin de parcours (proxy OFFSET 1.2M > 120 s, RÉFUTÉ) ; le keyset démarre où le lot précédent s'est arrêté → ~4,8 s/lot CONSTANT début↔fin (mesuré à vide 2026-05 ; en conditions réelles de cron ~16 s/lot ≈ 33-35 min pour ~1,31 M éligibles, prouvé runs 07→09/2026 — c'est le poste dominant du cron, d'où timeout-minutes: 120 ; un run tué par le budget = cancelledfailure, alerte dédiée dans ingest-rpps.yml). Jointure geocoded_addresses_pkey = nested-loop indexé optimal 0,18 ms/ligne → aucun index fonctionnel lourd sur rpps_staging requis (≠ l'ancien ingest_build_rpps_staging_ban_indexes, cause structurelle du blocage : CREATE INDEX multi-min via PostgREST = impossible, cap passerelle Supabase 60 s en dur). Séquence load-bearing scripts/ingest/rpps.ts : analyze (5a) → enrichment FINESS (5b) → rpps_count_ban_eligible_rows + runKeysetRpc(ingest_apply_rpps_ban_join_batch) (5c, fail-loud + sentinelle cohérence « 0 posé/cache non vide → throw ») → 5c-bis repli FINESS keyset ingest_apply_rpps_finess_centroid_fallback_batch (best-effort, même commune, migration 20260905T140000 ; retire ~5 900 clés (14 %) du périmètre BAN, dont ~3 700 rejetées jamais re-tentées — ban_eligible_distinct chute d'autant, ce n'est pas un progrès BAN) → swap → rebuildMatviews. Helper générique runKeysetRpc (shared.ts, garde de non-progression + withTimeout anti-hang). Expression rpps_address_key_for_index(...) + prédicat geom_source='commune_centroid' OR (geom IS NULL AND adresse IS NOT NULL) byte-identiques sur 6 sites (count / skip-scan ×2 / index staging ×2 / ban_join), gardés par ban-eligibility-predicate-parity (6 sites) + ban-eligibility-index-expr-parity (ban_join via WRAPPER, jamais le jumeau nu) + enrichment-statement-timeout (ban_join ≤55 s). runBanGeocodeStep SUPPRIMÉ (et ses ~8 constantes/imports BAN). Dette tracée : ingest_build_rpps_staging_ban_indexes conservée en base mais PLUS câblée par le cron ; ban-backfill.mjs (inchangé, hors scope) dépend encore des index BAN présents sur rpps — à résoudre dans la future feature « automatisation backfill » (post-swap bloquant = dead-end connu).
  • Acceptation BAN = par PRÉCISION (result_type), JAMAIS un gate binaire de score (fix 2026-05-19, prouvé prod ; détail docs/plans/ban-join.md + mémoire ban-acceptance-precision-tier). result_type (housenumber/street/locality/municipality) EST la garantie de précision géographique ; result_score n'est qu'une confiance fuzzy-match que les abréviations RPPS (R/BD/AV) + accents font chuter ALORS QUE le point est correct (prouvé : 500 rejetées re-géocodées, 81 % ont coords, ~90 % des housenumber 0,5–0,7 = bon bâtiment). L'ancien gate score≥0,7 ET type∈{housenumber,street} + ban-backfill.mjs mettant lat=NULL si non accepté jetait ~40k médecins au centroïde commune (~3 km) malgré un point rue/bâtiment BAN valide. Règle produit : rue/lieu-dit > centroïde. Fix : BAN_ACCEPT_SCORE 0,7→0,5 (scripts/ban-backfill.mjs) + type∈{housenumber,street,locality} (municipality exclu = aucun gain) dans src/core/ban-bulk-client.ts ; garde ban-bulk-client.test.ts. Le commentaire historique « JAMAIS 0.5 » (audit-P2) valait pour l'accept binaire-précis — la sémantique est désormais « upgrade vs centroïde », documentée inline (NE PAS reverter à 0,7 sans relire la preuve prod). Recovery = cache-only (geocoded_addresses hors swap, idempotent) ; ban_join du cron mensuel pose. Dette : re-géocodage récurrent encore manuel (cron ne géocode plus depuis suppression runBanGeocodeStep).
  • [OBSOLÈTE — superseded par ban_join ci-dessus, gardé pour le post-mortem] Ré-armement BAN via ingest_build_rpps_staging_ban_indexes() post-enrichment/pre-swap (refonte 2026-05-18) : la prémisse « un step RPC d'index dédié suffit » a été réfutée par la prod (run #26087010166 : CREATE INDEX multi-minutes via supabase-js → upstream request timeout, cap passerelle Supabase 60 s structurel). Leçon transverse conservée : indexer APRÈS chargement de masse reste juste (doc PostgreSQL « Populating a Database ») — mais via canal direct, JAMAIS via une RPC PostgREST synchrone dans le cron.
  • Valider un code/identifiant contre une matview FILTRÉE = faux positif sur les codes valides exclus par le filtre (post-mortem dette #1). rpps_savoir_faire_stats filtre WHERE profession_code IS NOT NULL ⇒ un savoir_faire_code n'apparaissant que sur des lignes profession NULL en serait absent → RangeError sur un code POURTANT valide (nouvelle panne silencieuse côté caller, pire que la dette). Toujours valider une nomenclature contre la matview NON filtrée qui est la source réelle du count que la validation protège (rpps_count_stats), pas une matview dérivée à finalité différente.
  • (date - date) retourne jours total ; pas EXTRACT(DAY FROM interval) (fragile, retourne le champ "day").
  • Coords = centroïde commune + recherche rayon = piège O(lignes/commune) (post-mortem V0.10.2). Une table dont les coords sont des centroïdes commune (RPPS) empile des dizaines de milliers de lignes au point identique en commune dense → ST_DWithin/KNN <-> par-ligne sur le cluster co-localisé = 15 s, l'index GiST n'élague rien (tous les points identiques passent le && bbox). Fix : matview de centroïdes communaux distincts (1 ligne/commune) → résoudre les communes dans le rayon (GiST sur la petite matview) puis CROSS JOIN LATERAL (... WHERE code_insee = c.code_insee ... LIMIT n) en early-stop via l'index B-tree code_insee. Jamais de KNN geog <-> point global sur une table à coords centroïde dense.
  • ORDER BY ST_Distance(geog, point) LIMIT N ne déclenche PAS le KNN GiST — utiliser ORDER BY geog <-> point (post-mortem V0.13.3, prouvé prod EXPLAIN ANALYZE Neuilly 2 km). PostGIS GiST supporte le KNN sur geography via l'opérateur <-> qui trie en streaming par bounding box + early-stop natif sur LIMIT. ORDER BY ST_Distance(...) au contraire force le planner à ramener TOUTES les lignes du bbox &&, recalculer la distance exacte par-ligne, puis top-N heapsort — coût O(N×K) où N = densité dans le bbox. Le piège V0.10.2 ci-dessus visait les centroïdes commune (cluster O(lignes/commune)) ; ici c'est le même piège inversé sur ban_address post-V0.13 (cache BAN à 1,14 M lignes, cluster Paris ouest dense 5–10 000 candidats à 2 km). Mesures prod rpps_in_radius(48.88, 2.27, 2000, ..., precise_only=true) : pré-fix 2 594 ms / 10 628 buffers → post-fix 56 ms / 1 393 buffers (×46 sur le temps, ×7,6 sur les buffers). Règle : distance_meters reste calculé via ST_Distance dans le SELECT (distance géodésique exacte, l'opérateur <-> retourne une distance bbox approximative — bonne pour le tri, mauvaise comme valeur publique). Vérification rapide qu'un plan utilise bien le KNN : chercher Order By: (geog <-> ...) dans Index Scan using <gist_index>, pas un Sort séparé.
  • La branche precise de rpps_in_radius exige un GiST PARTIEL WHERE geom_source IN ('finess_join','ban_address') (rpps_geog_precise_gist) ; un GiST GLOBAL sur rpps(geog) la re-régresse en 57014, et ingest_create_rpps_staging doit créer ce PARTIEL (jamais le global) sinon le swap reverte (post-mortem 2026-05-19, prouvé prod). Extension du piège V0.10.2 ci-dessus à la CTE precise : ST_DWithin(r.geog, v_point) filtré geom_source IN ('finess_join','ban_address'). Avec un GiST GLOBAL présent, le planner prend Index Scan rpps_geog_gist (geog && _st_expand) et relègue geom_source en Filter post-index → le bbox ramène tout le cluster co-localisé commune_centroid (prouvé Paris 1 km : 77 381 lignes dont 76 940 jetées en Filter pour 225 résultats) → 57014. 20260516T050000 DROP le global + CREATE le partiel sur rpps. Mais ingest_create_rpps_staging (def 20260518T140000, désamorçage cron) recopiait verbatim la def main créant le GiST global rpps_staging_geog_gist : au 1er swap le RENAME revertait rpps_geog_precise_gistrpps_geog_gist global = re-régression SILENCIEUSE (découplage des 2 firefights BAN-rearm vs désamorçage cron). Fix durabilité 20260519T160000 : staging-create crée rpps_staging_geog_precise_gist (partiel, prédicat byte-identique RPC↔20260516T050000↔guard), le swap le renomme en rpps_geog_precise_gist. Garde-fou staging-parity.test.ts (« tout GiST rpps_staging(geog) porte le prédicat partiel ») : forme POSITIVE sur CHAQUE GiST (geog) (≠ regex négative qui ratait IF NOT EXISTS/public./WITH/coexistence = faux VERT silencieux) + parité consommateur croisée vs rpps_in_radius + lecteur STRICT tag-aware latestFunctionBody(..., {stripComments:true}) du module (ferme le faux VERT « prédicat en commentaire inline » et « def future en $tag$ → corps mort capturé »). Le guard indexColumnLists historique est AVEUGLE ici (global et partiel normalisent à la même liste de colonnes geog, la clause WHERE étant hors du 1er groupe de parenthèses) — d'où l'assertion dédiée. Leçon transverse : 2 firefights concurrents peuvent découpler une fonction de son index compagnon ; tout index spatial sur rpps DOIT être mirroré PARTIEL-à-PARTIEL dans staging-create, pas seulement « par liste de colonnes ».
  • Cache paresseux rempli au serve-time → RLS doctrine anon lit / service_role écrit, JAMAIS d'écriture exposée à anon (V0.26 immobilier DVF, prouvé prod 42501 new row violates RLS). Toute table de cache remplie à la volée par l'endpoint public (dvf_mutations, dvf_commune_cache) : RLS ON + policy SELECT TO anon USING(true) pour le chemin de lecture (RPC SECURITY INVOKER tourne sous anon) + GRANT INSERT/UPDATE TO service_role pour les écritures via getUntypedServiceClient() (src/storage/supabase.ts), jamais de policy d'écriture anon (le rôle public polluerait le cache de prix). Registre interne (dvf_commune_cache) = 0 policy anon, lu+écrit en service. Miroir exact de geocoded_addresses (20260516T060000). 1ʳᵉ utilisation serve-time de service_role (avant : ingestion only) → SUPABASE_SERVICE_ROLE_KEY DOIT être dans l'env runtime Vercel (Production + Preview) sinon requireEnv throw au request-time (brique morte, échec LOUD). Le filet d'intégration env-gated DOIT tourner contre une VRAIE base RLS (clés ANON et SERVICE) au checkpoint deploy, sinon le lockout passe la revue en silence. Cf. [[immobilier-dvf-rls-lazy-cache]].
  • Upsert d'un CSV externe à PK composite → dédoublonner par PK AVANT ON CONFLICT (V0.26 DVF, SQLSTATE 21000 prouvé prod 50129 : 3107 lignes → 2073 clés). Le CSV geo-dvf partage la PK (id_mutation, code_commune, date_mutation, type_local) sur plusieurs lots d'une même vente → INSERT … ON CONFLICT rejette TOUT le lot (« cannot affect row a second time »). upsertMutations dédup en Map keep-last sur l'ENSEMBLE avant le batching (couvre la collision inter-batch) et retourne le nb réellement écrit (row_count cache honnête). Clé de dédup ET cible onConflict dérivées d'une source unique DVF_PK_COLS (as const satisfies readonly DvfStringKey[] — interdit une colonne PK non-string) ; toute divergence clé↔conflit ré-arme le 21000 en silence. Tests mockés/synthétiques distincts ne voient PAS ce bug → tester l'écriture DB contre ≥1 commune réelle.

Read the full file on GitHub · 30 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 5d ago First seen · 30 lines · 98 tokens per session scan A 72f704dcba4f

Subscribe to this mod's changes

db-gotchas is a skill published in the GitHub repository cturkieh/france-data-mcp (3 stars, last pushed 4d ago), licensed MIT. It adds 98 tokens to every session and 5,915 once invoked, about $0.0005 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-09-06.

Related

Other skills, from other repositories

triage-issues

Triage GitHub issues in the googleapis/mcp-toolbox repo: propose the correct labels (type / priority / product / status), check for duplicates, verify a bug has enough info to act on, and draft a triage comment. Use whenever a maintainer asks you to triage, label, categorize, prioritize, or "look at" an issue (or a…

googleapis/mcp-toolbox · 164 tokens

postgresql-indexing

PostgreSQL indexing best practices for Prowler: index design, partial indexes, partitioned table indexing, EXPLAIN ANALYZE validation, concurrent operations, monitoring, and maintenance. Trigger: When creating or modifying PostgreSQL indexes, analyzing query performance with EXPLAIN, debugging slow queries, reviewing…

prowler-cloud/prowler · 108 tokens

graphjin-eval

Create, extend, run, baseline, and diagnose GraphJin agent evaluations through the graphjin eval CLI.

dosco/graphjin · 27 tokens

axiom-audit-grdb-performance

Use when the user mentions GRDB performance review, slow GRDB queries, app-group database setup audit, a ValueObservation that stopped updating, or pre-release GRDB scan.

CharlesWiltgen/Axiom · 43 tokens

django-perf-review

Django performance code review. Use when asked to "review Django performance", "find N+1 queries", "optimize Django", "check queryset performance", "database performance", "Django ORM issues", or audit Django code for performance problems.

getsentry/skills · 55 tokens

mma-investigator

Expert system for investigating MMA (Multi-Metric Allocator) behavior on CockroachDB clusters. Helps oncall engineers diagnose load imbalances, understand rebalancing decisions, and identify why MMA did or didn't act.

cockroachdb/cockroach · 47 tokens