8 min

Vérifier un schéma PostgreSQL avant la première migration

La vérification d’un schéma PostgreSQL détecte les mauvaises correspondances, contraintes insuffisantes, index manquants et changements risqués avant que la première migration ne touche aux données.

Vérifier un schéma PostgreSQL avant la première migration

Un générateur IA peut produire du PostgreSQL valide tout en déduisant la mauvaise base de données. La syntaxe est la partie facile. Les erreurs dangereuses semblent plausibles : une relation facultative devient obligatoire, une chaîne de statut reçoit une contrainte CHECK incomplète, une suppression se propage à des enregistrements qui devraient survivre, ou une migration recrée une table et perd discrètement une colonne.

La vérification d’un schéma PostgreSQL doit donc tester séparément le sens, le comportement de la migration et la récupération. Je n’approuve un schéma déduit qu’après l’avoir confronté à un jeu de données connu, à des invariants explicites, à des requêtes représentatives, à une revue des changements destructifs et à une répétition de restauration. S’il manque l’un de ces éléments, la migration reste une proposition.

La première migration mérite ce niveau d’examen même si la base de production est vide. Les premières erreurs de schéma se figent vite, car le code de l’application, les données initiales, les rapports et les migrations ultérieures commencent à en dépendre. Un examen de quinze minutes avant la première exécution coûte généralement moins cher que d’expliquer six mois plus tard pourquoi deux concepts différents partagent une même colonne de texte nullable.

Un schéma déduit est une spécification non fiable

Considérez le schéma déduit comme un projet de spécification, pas comme une vérité exécutable. Le générateur a vu des prompts, des écrans d’exemple, des enregistrements importés ou du code d’application généré. Il n’a pas été témoin de toutes les exceptions métier, règles de conservation, importations en masse, corrections du support et paiements échoués que la base contiendra un jour.

Commencez par distinguer trois questions que les équipes confondent régulièrement. La justesse du schéma demande si les tables et les contraintes modélisent le domaine. La sécurité de la migration demande si les opérations proposées préservent les données existantes et maintiennent la base utilisable pendant leur exécution. L’état de préparation à la récupération demande si vous pouvez revenir à un état connu après une modification partielle ou sémantiquement incorrecte. Réussir sur un point dit peu de chose des deux autres.

Une instruction CREATE TABLE peut décrire la structure finale souhaitée et pourtant y parvenir par des opérations risquées. Supposons que le générateur transforme customer_name text en customer_id bigint. La clé étrangère finale peut être judicieuse, mais une migration qui supprime la colonne de nom avant de rapprocher les noms historiques de clients détruit la seule preuve nécessaire à ce rapprochement. La revue du schéma approuve la destination ; la revue de la migration examine le trajet.

Lisez le modèle proposé à voix haute dans le langage du domaine. Dites que chaque facture appartient à exactement un client légal, plutôt que de dire que invoices.customer_id référence customers.id. La première phrase invite à des objections utiles : des brouillons peuvent exister avant la sélection du client, des factures importées peuvent viser des clients archivés, et les documents légaux peuvent devoir figer le nom du client au moment de leur émission. Le vocabulaire SQL peut masquer ces désaccords.

J’exige une note d’hypothèses à côté de chaque table déduite. Elle doit préciser ce que représente une ligne, comment elle est identifiée, à qui elle appartient, si elle peut exister sans son parent apparent et ce que signifie sa suppression. Si l’équipe ne peut pas répondre à ces questions, le générateur a deviné une base de données que l’équipe n’a pas conçue.

Des enregistrements connus révèlent les mauvaises correspondances de tables

Un jeu de données connu doit contenir des enregistrements choisis pour leur couverture sémantique, car un grand échantillon aléatoire répète souvent le même cas simple. Dix enregistrements soigneusement sélectionnés peuvent révéler davantage que dix mille lignes presque identiques du scénario idéal.

Construisez une matrice de correspondance avant d’exécuter le DDL. Chaque ligne de la matrice doit suivre un concept source jusqu’à sa destination proposée et consigner le nombre ou la valeur attendue. Pour une application de commandes, l’artefact pourrait ressembler à ceci :

Fait connuDestination proposéeRésultat attendu
La commande A possède deux lignesorders et order_itemsUne ligne de commande et deux lignes enfants
La commande B n’a aucun compte attribuéorders.account_idUne ligne avec un compte NULL
Deux personnes partagent une adresse e-mailcontacts.emailLes deux lignes sont conservées, sauf si l’unicité est une règle déclarée
Le code produit comporte des zéros initiauxproducts.codeLa valeur texte 00417 reste inchangée
Une commande annulée conserve ses prélèvementsorders et chargesLes lignes de prélèvement restent après l’annulation

Cette étape détecte les erreurs de correspondance entre tables avant que les détails des contraintes ne détournent la revue. Les générateurs IA normalisent souvent les objets répétés dans des tables séparées, ce qui est généralement judicieux, mais la répétition ne prouve pas l’identité. Deux adresses de livraison ayant le même texte peuvent être des instantanés historiques plutôt que des références à une ligne d’adresse modifiable. Les fusionner signifie qu’une modification ultérieure de l’adresse réécrit l’historique.

L’erreur inverse existe aussi. Un générateur peut copier les champs client dans chaque commande parce que l’écran les affiche ensemble. Certaines valeurs appartiennent au client, alors que d’autres doivent rester un instantané de la commande. La bonne conception peut contenir à la fois customer_id et des champs du document émis tels que billing_name. Qualifier cela de duplication et supprimer l’un des deux côtés fait perdre soit l’identité actuelle, soit la vérité historique.

Chargez le jeu de données connu dans une base jetable via le même parcours d’importation ou d’initialisation que l’application utilisera. Écrivez ensuite des assertions sur les faits, pas seulement sur le nombre de lignes :

SELECT
    (SELECT count(*) FROM orders WHERE external_id = 'ORDER-A') AS order_a,
    (SELECT count(*) FROM order_items i
       JOIN orders o ON o.id = i.order_id
      WHERE o.external_id = 'ORDER-A') AS order_a_items,
    (SELECT account_id IS NULL FROM orders
      WHERE external_id = 'ORDER-B') AS order_b_unassigned;

La forme d’un résultat valide doit être explicite :

 order_a | order_a_items | order_b_unassigned
---------+---------------+---------------------
       1 |             2 | t

N’acceptez pas un écart inexpliqué sous prétexte que l’application générée s’affiche encore. Une interface peut cacher des parents dupliqués, des enfants supprimés, des codes tronqués et des valeurs par défaut inventées. Rapprochez chaque jeu de données préparé intentionnellement avant de parler de déploiement en production.

Les contraintes doivent exprimer les vérités métier

Une contrainte de base de données doit rejeter un état toujours invalide, quel que soit l’écran, l’API, l’importation ou le script de correction qui écrit la ligne. Lorsqu’une règle comporte des exceptions ou dépend de faits externes variables, la forcer dans une contrainte simple produit souvent soit du travail bloqué, soit des données trompeuses.

Les clés primaires identifient les lignes ; elles ne fournissent pas automatiquement une identité métier pertinente. Un identifiant interne bigint peut coexister avec un numéro de commande unique dans le périmètre d’un locataire. Si le métier dit que les numéros de commande sont uniques par locataire, UNIQUE (tenant_id, order_number) exprime cette règle. Une contrainte unique globale rejetterait des enregistrements légitimes, tandis que l’absence de contrainte permettrait l’ambiguïté lors de nouvelles tentatives.

Les contraintes CHECK conviennent aux faits stables d’une ligne, tels que quantity > 0 ou finished_at >= started_at. La documentation PostgreSQL explique que la base suppose qu’une expression CHECK est immuable pendant toute la vie de la contrainte. C’est pourquoi une contrainte CHECK appelant une fonction dont le comportement change plus tard peut laisser d’anciennes lignes violer la règle apparente. Utilisez une expression fixe pour une vérité fixe. Placez une politique variable, comme une liste d’éléments autorisés contrôlée par des administrateurs, dans une table référencée ou dans le parcours de l’application.

Les contraintes de statut générées exigent de la prudence. Un générateur peut examiner les exemples actuels et produire :

status text NOT NULL
    CHECK (status IN ('draft', 'active', 'closed'))

Cela n’est correct que si ces états sont les seuls états durables. Demandez ce qu’il en est des enregistrements échoués, annulés, suspendus, importés et historiques inconnus. Si la machine à états évolue encore, une table de référence peut rendre les ajouts explicites, mais elle ne remplace pas la validation des transitions. Une ligne autorisée à contenir closed ne dit rien sur la possibilité de passer directement de draft à closed.

Utilisez l’unicité de manière délibérée. PostgreSQL met en œuvre une contrainte unique avec un index B-tree unique, mais un index unique partiel exprime une règle différente. La suppression logique exige souvent l’unicité seulement parmi les lignes actives :

CREATE UNIQUE INDEX users_tenant_email_live_uq
    ON users (tenant_id, lower(email))
    WHERE deleted_at IS NULL;

Cela n’est pas interchangeable avec UNIQUE (tenant_id, email, deleted_at). PostgreSQL traite les valeurs NULL selon ses règles d’unicité, et l’ajout de l’horodatage de suppression modifie l’identité appliquée. Examinez les cas de doublon exacts avec des jeux de données préparés plutôt que de déduire le comportement de la liste des colonnes.

La possibilité de valeurs nulles est une décision métier

Définissez une colonne comme NOT NULL seulement lorsque le domaine exige une valeur pour chaque ligne légitime et que chaque parcours d’écriture peut la fournir. La conception d’un écran est une preuve faible. Un champ obligatoire dans le formulaire actuel ne dit rien des importations, brouillons, lignes générées par le système ou enregistrements historiques.

Examinez séparément quatre états : la source a omis le champ, la source a explicitement envoyé null, la source a envoyé une valeur vide et la source a fourni une valeur significative. Les API JSON, les formulaires, les importations CSV et PostgreSQL peuvent traiter ces états différemment. Si l’application ramène les quatre à un seul état avant l’insertion, la revue du schéma doit exposer cette décision au lieu de prétendre que la base l’a résolue.

Les valeurs par défaut méritent la même attention. Une valeur par défaut fournit une valeur lorsqu’un INSERT omet la colonne ; elle ne corrige pas un NULL explicite et ne prouve pas que la valeur est vraie. country_code DEFAULT 'US' est dangereux si un pays inconnu reste possible. La ligne contient alors un mensonge assuré auquel les rapports et la logique de conformité peuvent se fier.

Une migration générée courante ajoute une colonne obligatoire en une seule instruction :

ALTER TABLE customers
    ADD COLUMN account_type text NOT NULL DEFAULT 'standard';

L’instruction peut s’exécuter, mais chaque client historique devient standard sans preuve. Une séquence plus sûre ajoute la colonne nullable, déduit des valeurs à partir de données connues, mesure les lignes non résolues, empêche les nouvelles omissions dans les écritures de l’application, puis ajoute NOT NULL seulement si le domaine le permet. Si l’inconnu reste légitime, conservez NULL et définissez comment les requêtes et les interfaces l’affichent.

PostgreSQL offre une séparation utile pour certaines contraintes. Une contrainte CHECK ou une clé étrangère peut être ajoutée avec NOT VALID, ce qui évite de valider toutes les lignes existantes lors de la création, puis contrôlée ultérieurement avec VALIDATE CONSTRAINT. La documentation décrit cette option comme un moyen de reporter le parcours initial de la table. Ce n’est pas l’autorisation d’ignorer les anciennes violations : les nouvelles écritures sont contrôlées, et l’étape de validation doit tout de même réussir avant approbation.

Avant de renforcer la non-nullité, lancez une requête de répartition qui montre les catégories réelles :

SELECT
    count(*) AS total,
    count(*) FILTER (WHERE account_type IS NULL) AS nulls,
    count(*) FILTER (WHERE account_type = '') AS empty_strings,
    count(*) FILTER (WHERE account_type NOT IN
        ('standard', 'partner', 'internal')) AS unexpected
FROM customers;

Une valeur par défaut générée par IA peut rendre cette requête rassurante après la migration. Exécutez-la aussi avant le remplissage et conservez le résultat. Sinon, vous perdez la preuve nécessaire pour distinguer les valeurs déduites des valeurs inventées.

Les index doivent répondre à des accès observés

Répétez le parcours de récupération
Intégrez les instantanés et le rollback à un parcours de récupération testé pour les applications générées.

Approuvez un index s’il répond à une requête connue, garantit une règle d’unicité déclarée ou satisfait un besoin opérationnel. Indexer chaque colonne qui ressemble à un identifiant gaspille du stockage et du travail d’écriture, alors qu’un seul index composé important manquant peut transformer une page de liste ordinaire en analyse de plus en plus longue.

Commencez par les requêtes que l’application générée envoie réellement. Consignez les colonnes de filtre, la frontière du locataire, les colonnes de jointure, le tri et la taille attendue du résultat. Pour une page de commandes récentes, cette forme de requête importe plus que le diagramme de table :

SELECT id, order_number, status, created_at
FROM orders
WHERE tenant_id = $1
  AND status = $2
ORDER BY created_at DESC
LIMIT 50;

Un index sur tenant_id seul peut encore examiner de nombreuses lignes du locataire et les trier. Un index sur (tenant_id, status, created_at DESC) correspond mieux à ce parcours d’accès. L’ordre des colonnes ne se décide pas par popularité : il suit les conditions d’égalité, les conditions d’intervalle, le tri et la sélectivité de la requête réelle.

Exécutez EXPLAIN (ANALYZE, BUFFERS) sur des données représentatives, mais ne considérez pas un petit jeu de données comme une preuve de performance. PostgreSQL peut préférer à juste titre une analyse séquentielle sur une petite table. La vérification doit confirmer que l’index voulu existe et qu’une répétition à l’échelle de la production donne au planificateur un choix réaliste. Ne désactivez jamais les analyses séquentielles pour fabriquer une analyse par index afin d’obtenir une approbation.

Les clés étrangères apportent une autre surprise courante : PostgreSQL indexe les colonnes primaires ou uniques référencées, mais ne crée pas automatiquement d’index sur les colonnes enfants référençantes. La suppression ou la mise à jour d’un parent peut donc analyser la table enfant pour vérifier les références. Les jointures de l’enfant vers le parent peuvent aussi exiger cet index enfant. Examinez chaque relation selon les lectures attendues et les modifications des parents.

Rejetez les index dupliqués et inutilisés dans la proposition initiale. (tenant_id, status) peut être redondant lorsqu’un index adapté (tenant_id, status, created_at) existe déjà, même si les détails de la charge peuvent changer ce jugement. Comparez les définitions, pas les noms. Les générateurs IA créent souvent un index par fonctionnalité et ne remarquent pas que plusieurs fonctionnalités demandent les mêmes colonnes de tête.

Pour les bases existantes très sollicitées, rappelez-vous que CREATE INDEX CONCURRENTLY ne peut pas s’exécuter dans un bloc de transaction, demande plus de travail et peut laisser un index invalide après un échec. La documentation PostgreSQL détaille ces différences opérationnelles. Un framework de migration qui entoure chaque migration d’une transaction a besoin d’une exception explicite et d’une procédure de nettoyage, pas d’un remplacement optimiste de mot-clé.

Les clés étrangères exigent des règles de propriété et de suppression

Une clé étrangère n’est correcte qu’après que l’équipe a décidé si la relation exprime une propriété, une référence, un contexte facultatif ou une attribution historique. Des colonnes qui se ressemblent peuvent exiger des comportements de suppression opposés.

Considérez projects.owner_user_id, invoices.customer_id et audit_events.actor_user_id. Un projet peut changer de propriétaire. Une facture peut devoir survivre à la fermeture du compte client. Un événement d’audit peut préserver l’ancien identifiant de l’acteur après suppression des données d’identité. Appliquer ON DELETE CASCADE aux trois parce qu’elles font référence à users encoderait une fiction destructive.

Utilisez CASCADE lorsque l’enfant n’a aucun sens sans son parent et que supprimer le parent signifie réellement supprimer tout l’agrégat. Les lignes de commande conviennent souvent. Les enregistrements de paiement, documents émis, importations, journaux et preuves de modération ne conviennent souvent pas. Pour eux, le rejet, l’archivage, l’anonymisation contrôlée ou une référence nullable associée à des champs d’instantané conservés peuvent mieux convenir.

SET NULL exige aussi une revue sémantique. Cette option conserve la ligne enfant mais efface le lien direct. Si le personnel doit ensuite expliquer quel compte a créé un rapport, une référence nulle peut être insuffisante. Conserver un jeton historique non identifiant ou un instantané peut préserver la responsabilité sans conserver toutes les données personnelles, mais le choix précis de conservation relève de la politique produit, pas d’une supposition de l’IA.

Vérifiez la cardinalité dans les deux sens. Un générateur peut modéliser un à un en plaçant une clé étrangère sans contrainte unique, autorisant discrètement plusieurs lignes enfants. Il peut aussi imposer l’unicité là où l’historique exige plusieurs versions. Écrivez des cas de test pour un parent avec zéro, un et plusieurs enfants, puis indiquez quelles insertions doivent réussir.

Les contraintes différables exigent une raison précise. Elles peuvent aider lorsqu’une transaction doit temporairement violer l’ordre des références ou mettre à jour des lignes dépendantes les unes des autres, mais définir toutes les clés étrangères comme différées reporte les erreurs au commit et rend les échecs plus difficiles à localiser. Conservez l’application immédiate sauf si une vraie séquence de transaction exige un report.

Examinez le catalogue après avoir appliqué la migration à la base de répétition :

SELECT
    conname,
    contype,
    convalidated,
    pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'public.orders'::regclass
ORDER BY conname;

Un résultat représentatif a cette forme :

       conname        | contype | convalidated | definition
----------------------+---------+--------------+---------------------------------------
 orders_pkey          | p       | t            | PRIMARY KEY (id)
 orders_customer_fk   | f       | t            | FOREIGN KEY (customer_id) REFERENCES customers(id)
 orders_total_check   | c       | t            | CHECK ((total_cents >= 0))

Comparez les définitions renvoyées avec les règles de propriété approuvées. La réussite de la migration ne révèle pas à elle seule une action manquante, un report inattendu ou une contrainte non validée.

Les changements destructifs se cachent dans du SQL raisonnable

Examinez le parcours des données
Exportez le code source lorsqu’une migration générée exige un examen attentif des conversions et des remplissages.

Examinez la migration comme une transformation de données, car un DDL apparemment propre peut jeter du sens sans employer un DROP TABLE évident. Cherchez d’abord les destructions directes, puis examinez les conversions, remplissages, réécritures, renommages et remplacements de contraintes.

Un renommage et une suppression sont différents sur le plan opérationnel, même si le schéma final paraît identique. Si surname devient family_name, un renommage préserve plus fidèlement les données et les dépendances. Supprimer l’ancienne colonne puis ajouter la nouvelle produit le même diagramme, mais vide toutes les valeurs. Les migrations générées déduisent souvent l’état final sans comprendre la continuité.

Les changements de type nécessitent des conversions d’exemple et des cas de rejet. Transformer des identifiants texte en entiers peut supprimer les zéros initiaux ou rejeter des identifiants mixtes. Réduire la précision numérique peut arrondir des valeurs. Convertir des horodatages exige une hypothèse explicite sur le fuseau horaire. Testez l’expression USING réelle avec des valeurs minimales, maximales, nulles, malformées et historiquement inhabituelles avant de modifier la colonne.

Considérez ces opérations comme nécessitant une justification écrite : supprimer une table ou une colonne, changer un type par une conversion avec perte, remplacer une colonne renseignée, ajouter CASCADE, définir NOT NULL après un remplissage généré et reconstruire l’unicité avec d’autres colonnes. Examinez aussi le SQL brut incorporé dans les fonctions générées ou les callbacks de migration. La recherche textuelle est un filtre de départ, pas toute la revue.

Un schéma d’échec revient fréquemment. Les données connues contiennent des contacts dont l’entreprise est facultative, mais l’écran d’exemple ne montre que des contacts professionnels. Le générateur rend contacts.company_id NOT NULL et insère une entreprise générée nommée « Unknown » pour les lignes sans correspondance. La migration réussit, les comptes concordent et toutes les clés étrangères sont validées. Les données restent incorrectes : les contacts individuels semblent désormais appartenir à une entreprise, les rapports regroupent des personnes sans rapport entre elles et la suppression du substitut peut se propager à de vrais contacts.

La correction n’est pas une autre valeur par défaut. Restaurez l’état source, rendez la relation nullable, migrez seulement les correspondances appuyées par des preuves et ajoutez une assertion selon laquelle l’ensemble sans correspondance équivaut aux contacts individuels connus. Voilà pourquoi les jeux de données sémantiques doivent enregistrer les relations attendues, pas seulement le nombre de lignes attendu.

Les outils de diff de schéma aident, mais je déconseille une approbation fondée sur le seul diff. Cette recommandation est populaire parce qu’un diff est compact et facile à examiner. Il ne suffit pas comme unique contrôle, car il montre le changement structurel, pas la provenance des valeurs remplies, les limites de transaction, le comportement des verrous ou la vérité après migration.

Une répétition doit prouver les résultats et le comportement en cas d’échec

Exécutez la migration complète sur une restauration jetable du jeu de données connu, puis testez le résultat voulu et un parcours interrompu ou rejeté. Une nouvelle base vide aide à détecter les erreurs d’ordre, mais elle ne peut pas révéler une conversion avec perte, des lignes historiques invalides ou une validation lente.

Utilisez cette séquence de répétition comme artefact de livraison :

  1. Restaurez le jeu de données antérieur au changement dans une base isolée et consignez le nombre de lignes ainsi que les assertions sémantiques.
  2. Capturez le schéma actuel, appliquez l’artefact de migration exact et conservez toute la sortie avec les durées et les limites de transaction.
  3. Exécutez les contrôles du catalogue, les assertions de correspondance, les tests de rejet de contraintes et les requêtes d’application représentatives.
  4. Comparez les valeurs importantes aux attentes enregistrées, y compris les catégories sans correspondance et nulles.
  5. Exercez la méthode de récupération documentée, puis relancez les assertions antérieures à la migration sur la base récupérée.

Capturez un export du schéma seul avant et après :

pg_dump --schema-only --no-owner --no-privileges \
  --dbname "$DATABASE_URL" > schema.sql

Examinez les tables, séquences, index, contraintes, fonctions, déclencheurs, extensions et privilèges pertinents pour l’application. Un diff de modèle ORM peut omettre des objets que l’application ne modélise pas, notamment les déclencheurs, index d’expression, index partiels et fonctions installées manuellement.

Ajoutez des tests négatifs qui prouvent que les contraintes rejettent les états incorrects. Une transaction de test peut tenter une insertion invalide et annuler le résultat dans tous les cas :

BEGIN;

INSERT INTO order_items (order_id, quantity, unit_price_cents)
VALUES (1001, 0, 2500);

ROLLBACK;

La sortie attendue doit nommer la contrainte violée, par exemple :

ERROR:  new row for relation "order_items" violates check constraint "order_items_quantity_check"
DETAIL:  Failing row contains (..., 0, 2500, ...).

Ne comparez pas l’intégralité du texte d’erreur dans tous les environnements, car les détails peuvent varier. Vérifiez le SQLSTATE ou l’identité de la contrainte dans les tests automatisés et conservez une sortie lisible pour la personne qui relit.

Mesurez les verrous et la durée sur un jeu de données assez grand pour ressembler au déploiement prévu. Une opération qui se termine instantanément sur cinquante lignes peut bloquer les écritures lorsqu’elle en valide des millions. Pour une base de production initiale vide, le risque immédiat est plus faible, mais la répétition teste tout de même les données initiales importées et établit une base de référence pour les changements ultérieurs.

La récupération exige davantage qu’une migration inverse

Créez à partir de règles validées
Transformez des règles métier validées en application web, serveur ou mobile par chat.

Une récupération n’est crédible que si elle restaure les données et la compatibilité de l’application dans le délai que le service peut tolérer. Une migration inverse qui recrée des colonnes supprimées ne restaure pas leurs anciennes valeurs.

Choisissez l’unité de récupération avant l’exécution. Pour une base initiale vide, supprimer et recréer la base peut être acceptable si aucune écriture utilisateur n’a commencé. Une fois les vraies écritures présentes, la récupération peut exiger un instantané de base, une sauvegarde logique, la conservation des anciennes colonnes ou une réparation en avant. La bonne méthode dépend de la quantité de nouvelles données qui peuvent arriver pendant et après la migration.

Testez les commandes de restauration et les identifiants avant de vous y fier. Une sauvegarde qui existe mais que l’opérateur de déploiement ne peut pas restaurer n’est pas un plan de récupération. Restaurez dans une base distincte, vérifiez la propriété et les extensions, puis lancez les mêmes assertions connues qu’avant la migration.

Les instantanés et l’annulation de transaction résolvent des échecs différents. Une transaction peut annuler les instructions lorsqu’une migration échoue avant le commit, à condition que chaque opération participe à cette transaction. Un instantané peut ramener toute la base à un état antérieur, mais cela peut supprimer des écritures légitimes effectuées après l’instantané. Aucun de ces mécanismes ne rapproche automatiquement ces écritures.

Préférez les changements additifs lorsqu’une incertitude subsiste. Ajoutez la nouvelle colonne ou table, copiez les données selon des règles mesurables, exécutez les deux parcours de code pendant une période contrôlée si nécessaire, puis supprimez l’ancienne structure seulement après vérification. Cette approche d’extension puis de retrait demande davantage de travail, mais elle préserve les preuves. Conserver une ancienne colonne renommée pendant une version coûte souvent moins cher que de la reconstituer depuis les journaux.

Écrivez à l’avance les déclencheurs de récupération. Par exemple, toute assertion sémantique échouée, des enregistrements inattendus sans correspondance, une contrainte invalide, une migration dépassant sa fenêtre de verrouillage approuvée ou des erreurs d’application dues à une incompatibilité de version. L’opérateur ne doit pas inventer la décision pendant que les utilisateurs attendent.

Consignez le point après lequel restaurer l’ancienne base exige aussi de restaurer l’ancienne application. Une nouvelle application peut dépendre d’une nouvelle colonne, alors qu’une ancienne application peut rejeter une nouvelle valeur d’énumération ou écrire selon l’ancienne forme. La récupération de la base et celle de l’application doivent utiliser des versions compatibles.

L’approbation exige des preuves, pas de la confiance

Approuvez la première migration seulement si une autre personne peut reproduire les raisons de sa sécurité à partir des artefacts conservés. La confiance née d’une revue de code sans problème ou d’une interface générée soignée ne résiste pas au premier écart de données inexpliqué.

Le dossier d’approbation doit contenir les hypothèses déduites, la matrice de correspondance des tables, l’identité du jeu de données connu, le diff de schéma, la migration exacte, les requêtes et résultats de validation, les tests d’entrées rejetées, le raisonnement sur les index, les justifications des opérations destructives et la procédure de récupération testée. Nommez la personne qui relit et conservez les décisions non résolues comme des blocages au lieu de les enfouir dans l’historique de discussion.

Lorsqu’une application est générée dans Koder.ai, utilisez le mode planification pour consigner ces décisions de schéma avant d’autoriser la migration, exportez le code source pour revue et considérez les instantanés et le rollback comme des outils de récupération qui nécessitent tout de même une répétition sur le jeu de données connu.

Ne laissez pas le générateur approuver sa propre déduction en régénérant du code jusqu’à ce que les tests réussissent. Cette boucle peut faire en sorte que l’application se conforme à un schéma erroné au lieu de corriger le modèle. Une personne doit décider si la base de données correspond au domaine, surtout pour l’identité, la suppression, la conservation et les valeurs inconnues.

La requête finale d’approbation doit être ennuyeuse. Chaque fait connu correspond à un résultat attendu, chaque contrainte rejette le contre-exemple prévu, chaque opération destructive a une raison et la restauration reproduit les assertions antérieures à la migration. Si les preuves exigent une explication persuasive pour excuser un écart, arrêtez la migration. PostgreSQL appliquera le schéma avec précision, y compris les parties que le générateur a mal devinées.

FAQ

Que dois-je vérifier dans un schéma PostgreSQL généré par IA ?

Examinez le DDL généré, les opérations de migration et les hypothèses qui les sous-tendent. Un schéma final correct peut tout de même être obtenu par une migration qui supprime des données, bloque les écritures ou invente des valeurs par défaut trompeuses.

Quelle taille doit avoir un jeu de données de vérification du schéma ?

Utilisez un petit jeu de données qui contient des lignes ordinaires, des valeurs limites, des relations absentes, des doublons, des valeurs nulles, des chaînes vides et des anomalies historiques. Son but n’est pas le volume, mais de réfuter les hypothèses du générateur avant que les données de production ne le fassent.

Une migration de test réussie prouve-t-elle que le schéma est sûr ?

Non. Une migration réussie prouve que PostgreSQL a accepté les instructions dans cet état de la base. Elle ne prouve pas que les correspondances entre tables sont correctes, que les données ont conservé leur sens, que les index répondent aux vraies requêtes ou que la récupération fonctionne.

Quand une colonne PostgreSQL doit-elle être NOT NULL ?

Un champ ne doit être NOT NULL que si chaque enregistrement légitime a une valeur et si l’application peut la fournir dans chaque parcours d’écriture. N’utilisez pas une valeur par défaut inventée seulement pour satisfaire la contrainte, car elle remplace une donnée manquante visible par une fausse donnée crédible.

Dois-je utiliser une contrainte unique ou un index unique ?

Une contrainte unique exprime une règle à laquelle d’autres objets de la base peuvent faire référence, et PostgreSQL la soutient par un index. Un index unique est utile lorsque l’unicité ne s’applique qu’à certaines lignes ou expressions, comme les enregistrements non supprimés ou les adresses e-mail normalisées.

Les clés étrangères PostgreSQL créent-elles automatiquement des index ?

Indexez les colonnes qui servent à trouver les lignes parentes, filtrer les requêtes courantes, joindre de grandes tables ou garantir l’unicité. PostgreSQL n’indexe pas automatiquement le côté référençant d’une clé étrangère : examinez donc séparément les colonnes enfants au lieu de supposer que la contrainte s’en est chargée.

Dans quels cas ON DELETE CASCADE est-il sûr ?

Utilisez CASCADE seulement si la ligne enfant n’a plus de sens indépendant après la disparition de sa ligne parente. Si la suppression relève d’une décision métier ou si l’enfant constitue une preuve, comme une facture ou une trace d’audit, refusez ou traitez explicitement la suppression.

Comment détecter des changements destructifs dans une migration ?

Considérez chaque DROP, conversion restrictive, réécriture de table, nouvelle colonne obligatoire et contrainte remplacée comme potentiellement destructifs. Cherchez dans le texte de la migration, puis examinez aussi les fonctions générées et le SQL brut, car un comportement destructif peut s’y cacher.

Quelle est la manière la plus sûre de tester la récupération après migration ?

Restaurez la base avant modification dans un emplacement distinct, exécutez la migration, lancez des requêtes de vérification sémantique et comparez les résultats aux attentes enregistrées. Tester uniquement la migration inverse ne permet pas de retrouver les données supprimées et peut donner une fausse impression de sécurité.

Quelles preuves dois-je conserver après avoir validé un schéma ?

Conservez ensemble le DDL généré, le texte de la migration, le diff de schéma, les requêtes et résultats de vérification, la procédure de récupération et l’identité de la personne qui a relu. Ce dossier explique ce qui a été validé et aide la revue suivante à repérer les hypothèses qui ont changé.

Related posts