Interroger la base PostgreSQL de production TN (pod postgres-0, alias SSH belive-tn) en LECTURE SEULE via kubectl exec — lister information_schema.columns avant toute requête (piège snake_case / PascalCase), SELECT et EXPLAIN uniquement ; toute écriture se propose avec chiffres et suit le protocole sauvegarde + transaction + garde-fous. Utiliser pour toute question « qu'y a-t-il vraiment en base ? ».
Install
npx skillscat add slimhajjami-git/gisv2/prod-db Install via the SkillsCat registry.
/prod-db <question ou requête>
Demande reçue : $ARGUMENTS. Traduis-la en une ou plusieurs requêtes SQL
de lecture contre la base gis_v2 du serveur TN, exécute-les, et rends
les chiffres avec la requête utilisée. Si la demande contient (ou implique)
une écriture, saute directement à la section « Si une écriture est nécessaire ».
Les données sont l'actif du client : même du « junk » apparent peut avoir une
valeur (audit, litige) que tu ne connais pas. Un « continue » ou un « ok »
générique n'est jamais une autorisation d'écrire.
1. La commande exacte (lecture seule forcée)
Toujours passer par PGOPTIONS pour ouvrir la session en transaction lecture
seule : tout UPDATE/DELETE/INSERT/DDL y échoue aveccannot execute ... in a read-only transaction, même par erreur de frappe.
Requête courte :
ssh belive-tn "kubectl exec -i postgres-0 -n gisv2 -- env PGOPTIONS='-c default_transaction_read_only=on' psql -U postgres -d gis_v2 -X -v ON_ERROR_STOP=1 -c \"SELECT count(*) FROM vehicles WHERE company_id = 14\""Requête longue (heredoc → stdin traverse ssh puis kubectl exec -i) :
ssh belive-tn "kubectl exec -i postgres-0 -n gisv2 -- env PGOPTIONS='-c default_transaction_read_only=on' psql -U postgres -d gis_v2 -X -v ON_ERROR_STOP=1" <<'SQL'
SELECT s.id, s.name, st.code, s.billing_cycle, s.next_payment_amount, s.subscription_expires_at
FROM societes s
LEFT JOIN subscription_types st ON st.id = s.subscription_type_id
ORDER BY s.id;
SQLSur DZ (serveur de test) : même commande via ssh icosnet-dz "sudo kubectl exec -i postgres-0 -n gisv2 -- ...".
Options utiles : -A -F ';' pour un CSV brut, \x (mode étendu) pour une
ligne large, -t pour supprimer les en-têtes. Jamais psql -it interactif
depuis un script.
2. Lister les colonnes AVANT de requêter (obligatoire)
Le nommage des colonnes est incohérent d'une table à l'autre. Ne devine
jamais un nom de colonne : liste-le.
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'vehicles'
ORDER BY ordinal_position;Pour retrouver une table : SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY 1;
Le piège, avec des exemples réels :
| Table | Convention | Exemples |
|---|---|---|
vehicles |
snake_case… | company_id, mileage, plate_number, gps_device_id |
vehicles (échéances) |
…SAUF PascalCase entre guillemets | "InsuranceExpiry", "TaxExpiry", "TechnicalInspectionExpiry" |
vehicle_costs, maintenance_logs, fuel_entries, societes, subscription_types, gps_positions, gps_alerts |
snake_case | company_id, amount, odometer_km, recorded_at |
notifications, audit_logs, driver_scores, driving_events |
PascalCase entre guillemets | "CompanyId", "IsRead", "Timestamp", "CreatedAt" |
Sans guillemets, Postgres replie l'identifiant en minuscules : WHERE CompanyId = 14
donne column "companyid" does not exist. Écris WHERE "CompanyId" = 14.
-- vehicles : mélange des deux conventions dans la MÊME table
SELECT id, plate_number, mileage, "InsuranceExpiry", "TechnicalInspectionExpiry"
FROM vehicles WHERE company_id = 14 ORDER BY id;
-- notifications : tout en PascalCase
SELECT "Id", "Type", "IsRead", "CreatedAt"
FROM notifications WHERE "CompanyId" = 14 ORDER BY "CreatedAt" DESC LIMIT 20;3. Règles de requête
- SELECT, EXPLAIN,
\d,\dtuniquement. Rien d'autre ne sort de cette skill. - Toujours un
LIMITsur une table d'historique, et un filtre société
(company_id/"CompanyId") : la base est multi-tenant, ne mélange pas
les clients dans un même résultat sans raison. - Grosses tables :
gps_positions(~15 Go, > 12 M lignes) etgps_alerts
(~7,5 Go, noyée de télémétrie). Jamais de balayage sans filtredevice_id+ borne temporelle surrecorded_at. Pour « la dernière trame
par boîtier », préférer unLATERAL ... ORDER BY recorded_at DESC LIMIT 1
à unGROUP BY+max(). - Piège
gps_alerts.timestamp: sonmin()est en 2004 (horloges de
boîtiers corrompues). Pour dater une ligne, utilisercreated_atouid,
jamaistimestampseul. - Performance d'une requête de l'API : passer la requête réelle (celle du
log EF ou reconstituée) sousEXPLAIN (ANALYZE, BUFFERS)— elle s'exécute
vraiment, donc uniquement sur un SELECT, ce que la session lecture seule
garantit. Lirerows=réels vs estimés,Seq Scansur une grosse table,Buffers: shared read. Contexte :/vehicles/with-positionsest LE chemin
chaud (interrogé toutes les ~30 s par chaque utilisateur) — une requête
« seulement 250 ms » y ralentit toute l'application. - Données personnelles : ne remonte que les colonnes nécessaires
(pas d'emails/téléphones en masse), et n'en écris aucune dans un fichier du
dépôt. Les extractions vont dans le scratchpad. - Une question de schéma (« la colonne X existe-t-elle ? ») se répond par
information_schema.columns, pas par undotnet ef.
Requêtes de repérage fréquentes :
-- Sociétés, plan et échéance d'abonnement
SELECT s.id, s.name, st.code AS plan, s.billing_cycle, s.next_payment_amount,
s.subscription_expires_at, (SELECT count(*) FROM vehicles v WHERE v.company_id = s.id) AS parc
FROM societes s LEFT JOIN subscription_types st ON st.id = s.subscription_type_id
ORDER BY s.id;
-- Véhicules d'une société, avec ou sans boîtier
SELECT id, plate_number, mileage, gps_device_id IS NOT NULL AS a_un_boitier
FROM vehicles WHERE company_id = <id> ORDER BY id;
-- Dernière position par boîtier d'une société (LATERAL, pas de GROUP BY)
SELECT v.id, v.plate_number, p.recorded_at, p.latitude, p.longitude
FROM vehicles v
JOIN LATERAL (SELECT recorded_at, latitude, longitude FROM gps_positions gp
WHERE gp.device_id = v.gps_device_id ORDER BY recorded_at DESC LIMIT 1) p ON true
WHERE v.company_id = <id> AND v.gps_device_id IS NOT NULL;
-- Volumétrie des tables (sans les parcourir)
SELECT relname, n_live_tup, pg_size_pretty(pg_total_relation_size(relid)) AS taille
FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 15;4. Restituer
Pour chaque réponse : la requête exécutée (bloc de code), le résultat
(tableau ou chiffres), et l'interprétation en une ou deux phrases. Signale
explicitement ce qui t'a surpris (colonne inattendue, valeur incohérente comme
un settings->>'Currency' à DT), c'est souvent la vraie réponse à la question.
5. Si une écriture est nécessaire
Tu ne l'exécutes pas. Tu la proposes à Slim avec les chiffres, puis tu
attends une approbation explicite de cette opération précise.
Proposer : table, nombre exact de lignes (par
SELECT count(*)avec les
mêmes garde-fous que l'ordre prévu), identifiants, montants ou libellés
concernés, méthode (DELETE / UPDATE ciblé), plan de restauration (le
fichier JSON de sauvegarde + la sauvegarde nocturnepg_dumpdu volume/backups). Pour une purge de volume, ajouter la volumétrie et rappeler
l'écran/admin/database(sauvegardepresuppr_puis suppression par
lots) plutôt qu'un DELETE manuel.Attendre l'accord explicite (« ok enlève les doublons » = oui pour LES
doublons chiffrés, rien d'autre). Un « continue » n'est pas un accord.Re-vérifier juste avant d'agir (les lignes peuvent avoir bougé).
Sauvegarder les lignes dans le scratchpad, jamais dans le dépôt :
ssh belive-tn "kubectl exec -i postgres-0 -n gisv2 -- psql -U postgres -d gis_v2 -X -A -t -c \"SELECT row_to_json(t) FROM vehicle_costs t WHERE id IN (123, 456) AND company_id = 14\"" > "<scratchpad>/sauvegarde_vehicle_costs_$(date +%Y%m%d-%H%M%S).json"Contrôler que le fichier contient bien le nombre de lignes attendu.
Exécuter dans une transaction avec garde-fous complets — c'est la
SEULE commande de cette skill sansPGOPTIONSlecture seule. Le WHERE
porte l'identifiant ET la société ET le libellé ET le montant ; un blocDO $$compareROW_COUNTà l'attendu et annule tout si ça diffère.
Le hookguard-bashbloquera cette commande (DELETEviapsql) : soit Slim
l'exécute lui-même, soit il crée lui-même.claude/hooks/.unlock-destructive-sql
(fenêtre de 15 min) — Claude ne crée jamais ce fichier.
Premier passage avecROLLBACKpour voir l'effet, second avecCOMMIT:ssh belive-tn "kubectl exec -i postgres-0 -n gisv2 -- psql -U postgres -d gis_v2 -X -v ON_ERROR_STOP=1" <<'SQL' BEGIN; DO $$ DECLARE n integer; BEGIN DELETE FROM vehicle_costs WHERE id IN (123, 456) AND company_id = 14 AND description = 'FILTRE CLIM' AND amount = 85.00; GET DIAGNOSTICS n = ROW_COUNT; IF n <> 2 THEN RAISE EXCEPTION 'Attendu 2 lignes, obtenu % : annulation', n; END IF; END $$; SELECT count(*) AS restantes FROM vehicle_costs WHERE id IN (123, 456); ROLLBACK; -- remplacer par COMMIT au second passage, une fois le compte vérifié SQLMême gabarit pour un
UPDATEde recalage (avecAND colonne IS DISTINCT FROM nouvelle_valeur
dans le WHERE pour que le compte attendu soit exact).Vérifier après par un SELECT (lignes absentes / valeurs recalées) et
consigner dans le compte rendu : ids, nombre, chemin du fichier de sauvegarde.
Un changement de schéma (colonne, table, index) n'est pas une écriture ad hoc :
il passe par un fichier migrations/0NN_*.sql (/migration) joué par /deploy
AVANT le pod API.
Ne jamais
- Exécuter
UPDATE,DELETE,INSERT,TRUNCATE,DROP,ALTER,VACUUM FULL
de sa propre initiative, même sur des lignes « manifestement » inutiles. - Considérer un « continue », un « ok » ou un « vas-y » générique comme une
autorisation d'écrire : l'accord porte sur une opération chiffrée précise. - Omettre
PGOPTIONS='-c default_transaction_read_only=on'sur une session de lecture. - Deviner un nom de colonne sans avoir listé
information_schema.columns. - Balayer
gps_positionsougps_alertssans filtre boîtier + borne temporelle, ni sans LIMIT. - Dater
gps_alertssur la colonnetimestamp(horloges corrompues, min en 2004). - Écrire un extrait de base (surtout emails, téléphones, montants clients) dans un fichier du dépôt.
- Utiliser
dotnet efpour lire ou modifier le schéma de prod. - Lancer
docker system prune -a --volumesou toute commande de nettoyage sur
le serveur au passage : les anciens volumes docker-compose contiennent
peut-être une ancienne base.