


Développer une extension d'index de base de données qui passe les tests et résiste aux crashs
À utiliser quand une IA crée ou modifie une extension d'index de base de données. Elle traite chaque échec de test comme réel, suit un ordre fixe avant le commit et journalise chaque changement de page pour que la reprise et les instances de secours fonctionnent.
Pour utiliser ces règles d'IA
## Redémarrer le serveur après réinstallation et lire d'abord la spécification de stockage
Vous contribuez à une extension de base de données qui implémente une méthode d'accès par index adossée à un stockage sur disque en couches, à l'intérieur des propres fichiers de relation de la base de données. Ce travail touche de près à la durabilité, à la récupération et à la concurrence, donc l'exactitude prime sur la rapidité de livraison dans chaque décision.
Hypothèses opérationnelles à respecter en permanence :
- L'extension est chargée au démarrage du serveur. Il faut l'ajouter à la configuration de préchargement du serveur et redémarrer avant de pouvoir créer l'extension, et un redémarrage supplémentaire est nécessaire après avoir réinstallé le binaire en cours de développement. Un binaire chargé obsolète produit des résultats de test trompeurs ; quand le comportement contredit le code source que vous venez de construire, soupçonnez d'abord un redémarrage manquant.
- Installez les objets de l'extension dans un schéma dédié plutôt que dans l'espace de noms public par défaut chaque fois que le projet le permet, afin de garder l'espace de noms propre.
- Privilégiez le plus petit changement qui résout entièrement le problème. Ne refactorisez pas le code adjacent que la tâche ne demandait pas.
- Les spécifications de conception présentes dans le dépôt font foi. Lisez la spécification de stockage avant de toucher au chemin d'écriture, de lecture ou de débordement, et mettez à jour la spécification dans le même changement lorsque la conception elle-même évolue.
## Examiner chaque échec de test au lieu de le dire sans rapport
La branche principale reste continuellement verte. Chaque échec de test compte.
- Ne rejetez jamais un échec en le qualifiant de préexistant ou de sans rapport. En pratique, ce n'est presque jamais le cas.
- Si vous pensez sincèrement qu'un échec est sans rapport avec votre changement, vérifiez-le : basculez sur la branche principale et exécutez-y le même test.
- Reproduire l'échec sur la branche principale ne l'excuse pas. Il doit tout de même être étudié et corrigé, et non classé ou annoté comme connu.
- Ne marquez jamais un test comme ignoré, n'assouplissez jamais une assertion et ne modifiez jamais un fichier de sortie attendue simplement pour faire passer une exécution en échec au vert. Ne changez une attente qu'après avoir pu expliquer précisément pourquoi la nouvelle sortie est correcte.
- Signalez les échecs honnêtement et tôt, y compris ceux que vous avez causés. Dissimuler une exécution en échec est pire que le bug lui-même.
## Lancer les tests de régression et lire le fichier de différences avant chaque commit
Terminez chaque étape ci-dessous avant de valider un changement. Ne réordonnez pas et n'omettez pas d'étapes parce qu'un changement « paraît trivial ».
1. Compilez l'extension.
2. Exécutez la suite de régression déclarative sur une version installée.
3. Inspectez le fichier de différences de régression produit par l'exécution, pas seulement le résumé de réussite ou d'échec. Une suite peut signaler un succès tout en masquant une différence que vous avez introduite.
4. Si vous avez modifié un message visible par l'utilisateur, mettez à jour les fichiers de sortie attendue correspondants pour qu'ils correspondent, et indiquez dans le message de validation pourquoi le message a changé.
5. Exécutez la vérification de formatage.
6. Relancez la suite de régression après toute mise à jour d'un fichier d'attente, afin que l'exécution finalement enregistrée soit bien celle qui reflète l'arborescence validée.
Au-delà de la suite habituelle, exécutez les niveaux concurrence, récupération après crash, multi-processus et charge intensive chaque fois qu'un changement touche au verrouillage, au chemin d'écriture, à la récupération ou au cycle de vie des segments. Indiquez clairement quels niveaux vous avez exécutés et lesquels vous n'avez pas exécutés.
## Placer chaque modification dans la couche de code la plus basse qui la résout
Le code source est organisé en couches, et chaque changement appartient à la couche la plus basse qui le résout entièrement. Ne laissez pas de détails de stockage remonter dans la couche d'interface avec la base de données, et ne descendez pas au-delà d'une abstraction pour raccourcir une correction.
- Couche un, interface avec la base de données : le gestionnaire de méthode d'accès, la construction d'index, le parcours et les points d'entrée de nettoyage (vacuum) ; les types de données personnalisés ; les points d'ancrage du planificateur et l'estimation de coût.
- Couche deux, coordination de l'index : le classement et le calcul de score, l'optimisation du top-k, le cycle de vie de l'état de l'index, le registre, la gestion de la métapage et l'abstraction sur les sources de listes de postings.
- Couche trois, stockage : le tampon d'écriture de niveau zéro, les segments immuables, la fusion et la compression.
- Transversal : les utilitaires de débogage et de vidage, et l'initialisation du module.
Lorsqu'un changement traverse plusieurs couches, décrivez explicitement le franchissement de frontière dans la description du changement. Une correction qui élargit discrètement une interface est un changement de conception et doit être revue comme tel.
## Comprendre le tampon d'écriture et les segments immuables avant de toucher au stockage
L'index suit une approche de stockage en couches, structurée en journal. Comprenez le modèle avant d'en modifier une quelconque partie.
- Le niveau zéro est un tampon d'écriture : une chaîne de pages orientée ajout, stockée à l'intérieur de la relation d'index elle-même et ancrée sur une métapage. Les écritures ajoutent des enregistrements portant l'identifiant de ligne, une longueur et une charge utile compacte à la page de queue. Toutes les mutations se font sous des verrous de tampon standard et sont journalisées de manière anticipée (write-ahead logging).
- Les segments sont des structures immuables organisées en blocs sur disque, portant des entrées de saut permettant à un parcours d'atteindre directement un bloc sans devoir passer par chaque entrée de posting.
- Le débordement du tampon d'écriture vers un nouveau segment est déclenché par un seuil, et les segments sont compactés par niveau une fois qu'un niveau en accumule suffisamment.
- Les requêtes top-k utilisent des bornes supérieures au niveau des blocs, de sorte que les blocs qui ne peuvent pas contribuer au résultat sont entièrement ignorés, avec une terminaison anticipée dès que la borne restante ne peut plus dépasser l'ensemble de résultats actuel.
Un cache en mémoire peut servir les lectures de requêtes pour la rapidité, mais la chaîne durable sur disque reste l'unique source de vérité, et les instances de secours en lecture seule lisent toujours la chaîne durable plutôt qu'un cache.
## Journaliser chaque changement de page via la journalisation anticipée standard, pas un mécanisme maison
Journalisez chaque mutation de page via le mécanisme générique de journalisation anticipée de la base de données, plutôt que d'enregistrer un gestionnaire de ressources personnalisé pour l'extension.
Pourquoi cette contrainte existe : avec la journalisation générique, le mécanisme de rejeu standard de la base de données reconstruit chaque page pendant la récupération après crash et sur une instance de secours en flux continu, y compris les pages de la chaîne du tampon d'écriture, les pages de segment et la métapage. C'est précisément ce qui permet aux assistants de rejeu de page unique, ainsi qu'à tout autre contexte de rejeu incapable de charger la bibliothèque partagée de l'extension, de fonctionner correctement.
Conséquences à respecter :
- Introduire un gestionnaire de ressources personnalisé est une décision de conception majeure, pas un détail d'implémentation. Cela casse tout contexte de rejeu incapable de charger l'extension. N'en introduisez jamais un implicitement dans le cadre d'un changement sans rapport ; soulevez la question comme une proposition distincte avec sa propre revue.
- Toute nouvelle structure sur disque doit pouvoir être reconstruite par le rejeu standard à partir des seuls enregistrements journalisés. Si une structure ne peut être reconstruite que par le code de l'extension, la conception est erronée.
- Ne contournez jamais le gestionnaire de tampons et n'écrivez jamais de pages en dehors d'une mutation journalisée, même pour une structure que vous jugez jetable.
## Garder les pages libérées par une fusion jusqu'à ce que les instances de secours ne les lisent plus
Une fusion ne doit pas restituer immédiatement les pages source déplacées à la carte d'espace libre. Le faire est sûr sur l'instance principale mais dangereux pour les requêtes de lecture en cours sur une instance de secours active, car sans gestionnaire de ressources personnalisé il n'existe aucun mécanisme pour résoudre les conflits de récupération pendant le rejeu : l'instance de secours réutiliserait des pages qu'une requête en cours est encore en train de lire.
Le motif requis :
- Garez les pages déplacées dans une chaîne de pierres tombales journalisée, ancrée sur la métapage, et marquée avec l'identifiant de transaction de la fusion.
- Ne restituez les pages garées à la carte d'espace libre que lorsqu'un nettoyage (vacuum) ultérieur, ou la fusion suivante, constate que la marque précède l'horizon des transactions les plus anciennes non supprimables. Cet horizon est la frontière de récupération sûre pour les instances de secours.
- Exigez que le retour d'information des instances de secours actives servant des requêtes soit activé dans le déploiement. Leur instantané le plus ancien retient alors l'horizon de l'instance principale jusqu'à ce qu'elles aient fini de lire les pages garées.
- Exposez une fonction de diagnostic qui indique combien de pages sont actuellement garées, afin que les opérateurs puissent observer la croissance de l'arriéré au lieu de le deviner.
Généralisez le principe : lorsqu'un composant libère une ressource partagée qu'un lecteur distant ou différé pourrait encore détenir, différez la libération derrière un horizon observable et donnez aux opérateurs un moyen de mesurer l'ensemble différé.
## Trier par score de classement au lieu de filtrer dessus
Les scores de classement existent pour ordonner, pas pour filtrer. Leurs valeurs numériques sont opaques et ne portent aucun seuil significatif, donc comparer un score à une constante est un anti-motif chaque fois qu'il sert de filtre de commodité dans les tests, les exemples ou la documentation.
Règles :
- N'écrivez pas de condition de filtre comparant un score de classement à un nombre pour sélectionner des lignes « correspondantes ».
- Une condition de filtre contenant l'opérateur de classement déclenche un calcul de score autonome via un parcours séquentiel plutôt qu'un parcours d'index. Les parcours d'index nécessitent une clause de tri.
- Le motif correct consiste à trier selon l'expression de classement et à appliquer une limite de lignes.
- Pour compter les documents correspondants, encapsulez une sous-requête triée et comptez ses lignes, plutôt que de filtrer sur le score.
- Exception : un test dont la cible explicite est le chemin de calcul de score autonome, par exemple un test qui compare les résultats d'un parcours d'index à des résultats autonomes, peut utiliser l'opérateur dans une condition de filtre. De tels tests doivent indiquer cette intention dans un commentaire afin qu'un lecteur ultérieur ne reproduise pas le motif par erreur.
La leçon générale : lorsqu'une API a une seule forme prévue, rendez la mauvaise forme visiblement fausse dans chaque exemple que l'équipe publie, et ne prévoyez des exceptions qu'avec une intention documentée.
## Documenter chaque réglage dans un seul tableau avec une valeur par défaut sûre
Chaque paramètre ajustable à l'exécution est documenté dans une table unique avec son nom, sa description et sa valeur par défaut. Un paramètre non documenté est traité comme un défaut.
- Choisissez des valeurs par défaut sûres et neutres pour un déploiement ordinaire. Utilisez zéro pour signifier « désactivé » pour les paramètres de type seuil et « illimité » pour les paramètres de type budget, et précisez-le dans la description.
- Les paramètres qui injectent des fautes ou forcent des échecs n'existent que pour les tests, doivent être réservés aux utilisateurs privilégiés, et doivent être signalés comme réservés aux tests dans leur description.
- Les commutateurs de journalisation pour le détail du calcul de score, les statistiques d'optimisation et les résultats de cache sont désactivés par défaut ; ce sont des aides au diagnostic, pas un fonctionnement normal.
- Les budgets mémoire sont échelonnés plutôt que d'être un mur unique et rigide : un plafond souple par index fait basculer cet index vers le chemin de repli durable, un plafond souple global évacue le plus gros consommateur autre que l'appelant, et un plafond rigide global refuse purement et simplement les nouvelles allocations. Chaque niveau doit être observable dans les journaux.
- Les options au niveau de l'index, telles que la configuration d'analyse de texte et les paramètres de saturation et de normalisation de longueur du modèle de classement, sont documentées avec la même rigueur, et les options requises sont marquées comme requises plutôt que de recevoir une valeur par défaut silencieuse.
## Marquer les documents morts et noter que les statistiques restent périmées jusqu'à la prochaine fusion
Le nettoyage (vacuum) marque les documents morts dans un ensemble de bits « vivant » par segment, un bit par document, plutôt que de reconstruire le segment. Le coût est proportionnel au nombre de documents morts plutôt qu'à l'ensemble du corpus. Les documents morts sont filtrés lors du calcul de score et physiquement supprimés à la fusion de segments suivante.
Le compromis connu doit rester documenté plutôt que caché : marquer des documents comme morts ne met à jour ni le nombre total de documents du segment, ni les statistiques de longueur totale, ni la fréquence documentaire par terme. Le classement utilise donc des statistiques de corpus légèrement obsolètes jusqu'à ce que la fusion ou la compaction suivante les corrige. Pour un petit nombre de suppressions, l'effet est négligeable ; une suppression massive peut affecter visiblement la qualité du classement jusqu'à la fusion suivante.
Traitez ceci comme un modèle. Lorsqu'une optimisation échange précision ou fraîcheur contre coût, écrivez ce compromis là où le mainteneur le trouvera, indiquez les conditions dans lesquelles il devient significatif, et nommez l'événement qui le résout. Une approximation non documentée devient plus tard un rapport de bug inexpliqué.
## Maintenir des niveaux de test distincts et des fonctions de diagnostic intégrées
Maintenez des niveaux de test distincts et sachez ce que chacun protège :
- Des tests de régression déclaratifs pour les fonctionnalités de base, l'exactitude du calcul de score, le stockage et les segments, les chemins d'optimisation de requêtes, les variantes de tables telles que les tables partitionnées et héritées, et les cas limites, avec des sorties de référence conservées sous contrôle de version.
- Des tests pilotés par script pour la concurrence multi-session, la récupération après crash, le comportement des segments multi-processus, et la charge intensive de longue durée.
L'intégration continue couvre chaque version majeure de base de données prise en charge, les builds avec sanitizers, la validation du formatage, les tests de mise à niveau de version, l'empaquetage des versions publiées, les benchmarks, et une exécution nocturne de charge intensive avec détection de fuites mémoire.
Livrez les diagnostics comme des fonctions de première classe, pas comme des impressions improvisées : videz la structure interne avec son dictionnaire de termes, ses listes de postings et ses entrées de documents ; videz l'index complet dans un fichier ; résumez les statistiques de haut niveau ; forcez un débordement et rapportez combien d'entrées ont été déplacées ; rapportez le nombre de pages garées ; et affichez une visualisation de la disposition des pages.
Gardez les benchmarks reproductibles : une petite collection classique de recherche d'information pour un signal rapide, des jeux de données publics plus grands pour l'échelle, un script de stress mémoire et de capacité, et une comparaison avec un moteur externe établi pour valider que les résultats ne sont pas simplement cohérents avec eux-mêmes.
## Utiliser le formateur automatique et rebaser sur main avant un push
- Le formatage est automatisé. Fournissez et utilisez des commandes pour formater l'arborescence, vérifier le formatage sans modifier, afficher les différences, et formater un seul fichier. Ne formatez jamais à la main en contournant l'outil.
- Suivez les conventions de code et les schémas d'allocation mémoire du projet de base de données hôte plutôt que d'en inventer de locales, et limitez la longueur des lignes à la largeur configurée par le projet.
- Invariants de concurrence : chaque structure partagée est protégée par un verrou approprié, les structures d'internement sont rendues sûres via des verrous de table de hachage, et l'isolation des transactions est maintenue par un nettoyage discipliné sur les chemins de validation comme d'annulation.
- Après la fusion d'une pull request, vérifiez toujours si la branche principale a bougé, même quand quelqu'un d'autre a effectué la fusion. Récupérez et rebasez les branches actives sur la branche principale mise à jour pour éviter d'accumuler des conflits.
- Avant de pousser, vérifiez que la branche est à jour avec la branche principale afin que des historiques divergents ne transforment pas un petit changement en une fusion chargée de conflits.
- Suivez la procédure de publication documentée du dépôt plutôt que d'improviser des étiquettes ou des artefacts.
