Aller au contenu principal

L'instantané SQL du corpus

_source-oldproject/ n'entre ni dans une image Docker, ni dans un runner de CI. Sans autre mécanisme, un environnement déployé n'aurait donc ni technologie, ni message type, ni gabarit : une base au schéma complet et vide de tout catalogue. L'instantané SQL comble ce trou. Il vit dans packages/database/prisma/seed/corpus/, il est versionné en clair, et c'est lui que sèment la CI et les trois environnements.

Une commande le régénère :

pnpm db:snapshot [chemin-du-corpus]

Le chemin est optionnel : à défaut, _source-oldproject à la racine du dépôt. Les arguments suivants sont transmis tels quels à l'import — pnpm db:snapshot ../ailleurs --overwrite-templates.

Pourquoi photographier plutôt que porter​

Le corpus est hors dépôt : il porte les clés d'intake de production des 65 communities SEKOIA, et le .gitignore comme le .dockerignore l'écartent. Son sondage, ensuite, lance un interpréteur Python — l'import n'y lit pas des fichiers, il appelle réellement les générateurs. Ni le dossier ni Python n'ont leur place dans une image de production.

Le SQL produit ici ne demande rien de tout cela : il se rejoue avec une connexion PostgreSQL, et c'est tout.

Il ne remplace pas l'import, il le photographie

La source de vérité reste le corpus. export-corpus.ts lit la base après que l'import l'a écrite, si bien qu'aucune règle d'assemblage, de réécriture ou d'identification de pool n'est réimplémentée à cet endroit. C'est ce qui empêche les deux chemins de diverger : il n'y a qu'un seul lecteur du corpus, et ce n'est pas l'exportateur.

Les trois gestes de la commande​

#GestePourquoi
1Build de @sekoiacraft/databaseLes seeds l'importent par son nom, donc via exports, donc depuis dist/
2prisma/seed/import-source.ts <corpus>Écrit le corpus en base — c'est l'import ordinaire, celui de pnpm db:seed
3prisma/seed/export-corpus.tsRelit la base et l'écrit en SQL, un fichier par technologie

L'ordre est délibéré : elle importe, puis photographie. Exporter sans réimporter figerait ce que la base contient au moment du geste — c'est-à-dire, un jour ou l'autre, un corpus périmé pris pour le corpus courant.

Elle exige le corpus, là où pnpm db:seed s'en passe​

C'est la différence qui compte entre les deux commandes. pnpm db:seed choisit : le corpus quand il est présent, l'instantané sinon. pnpm db:snapshot refuse : sans corpus, elle sortirait en code 1 plutôt que de recopier l'instantané existant en se présentant comme une régénération.

Trois pré-requis, et une base intermédiaire

Le corpus, python3, et un DATABASE_URL joignable. La commande a besoin d'une base de travail : elle y importe, puis la relit. Contrairement à db:push et db:reset, elle ne vérifie pas que la cible est locale — l'import n'écrit que par upsert et ne drope rien —, mais la cible est annoncée avant de partir. Lisez-la.

Ce qu'elle produit​

Le répertoire est reconstruit à chaque passage : une technologie retirée du corpus doit voir son fichier disparaître, faute de quoi le chargement la recréerait.

FichierContenu
_shared.sqlLes pools qu'aucune technologie ne porte en propre et les six populations de démonstration. Chargé en premier : des messages types de plusieurs technologies s'y rattachent
<technologie>.sqlUne technologie, ses jeux de données, ses messages types et leurs gabarits
manifest.jsonL'ordre de chargement, les comptes que le chargement doit produire, et l'empreinte des règles qui ont façonné les gabarits

Aujourd'hui 30 fichiers, environ 12 Mo, du plus gros (sekoia-agent.sql, 4,6 Mo) au plus petit (office-365-message-trace.sql, 20 ko). Chaque fichier s'ouvre sur un en-tête qui dit son volume et rappelle qu'il est généré — à ne pas éditer à la main.

Les lignes sont écrites par point de code, jamais dans l'ordre de l'ORDER BY du serveur : celui-ci suit la collation, et PGlite (C) et un PostgreSQL en en_US.UTF-8 — qui ignore les tirets — rangent windows.sql et bind.sql différemment. Le même corpus donne donc les mêmes fichiers, quelle que soit la base intermédiaire.

Ce que le manifeste annonce :

Comptes du manifesteValeur
Technologies28
Jeux de données398
Enregistrements10 878
Messages types474
Gabarits474

Le SQL est rejouable : chaque INSERT porte son ON CONFLICT … DO UPDATE. Recharger une base déjà chargée la met à jour au lieu d'échouer. Les identifiants sont des gen_random_uuid(), jamais des UUID figés, et un jeu de données est retrouvé par son slug — un identifiant figé se périmerait au premier réimport.

L'instantané photographie la base, pas seulement le corpus

D'où l'écart avec les chiffres de la page Importer le corpus, qui décrivent le corpus lui-même. export-corpus.ts lit ce qui est en base au moment du geste : les messages types du corpus, moins celui qui en est écarté, plus les quatre gestes d'administration Netskope que seed-netskope-admin.ts ajoute, plus les trois catalogues que le dépôt possède en propre (web-destinations, mail-client-process, script-process) et qu'aucun pool du corpus ne fournit.

L'ordre de chargement est calculé, pas alphabétique​

Un message type peut se rattacher au pool d'une autre technologie — deux le font aujourd'hui, chez SentinelOne Cloud Funnel vers un pool de Github Audit Logs. Le fichier qui crée le pool doit donc passer avant celui qui s'y rattache, et c'est pourquoi sentinelone-cloud-funnel.sql ferme la marche là où l'alphabet le placerait au milieu.

L'ordre est un tri topologique sur une seule sorte d'arête — « un message type de A se rattache à un pool de B, donc B avant A » —, départagé alphabétiquement pour qu'un corpus inchangé rende toujours le même ordre. Il est écrit dans manifest.json, et le chargeur s'y tient.

Le mode de panne que cela évite est muet, et c'est ce qui justifie un garde-fou plutôt qu'une règle : un rattachement non résolu ne fait pas échouer le chargement, il laisse le gabarit rendre avec sample = {} — donc du JSON invalide, dont l'intake n'extrait plus aucun champ et pas une smart description ne se déclenche. tests/corpus-snapshot.spec.ts tient l'invariant : aucun jeu de données n'est cité avant d'avoir été créé.

Ce que le SQL ne porte pas, délibérément​

AbsentPourquoi
EventTypeAction (la couverture)Déduite en code par deriveActionCoverage(), qui tourne sur les deux chemins
EventType.scope / scopeOriginÉcrits par la même passe, pour la même raison
Les parents des pools de premier planPosés par completePoolParents(), également sur les deux chemins
createdAtLa colonne a un défaut ; l'émettre ferait bouger le diff de tous les fichiers à chaque export

Les figer reviendrait à empêcher une correction du code de prendre effet sur une base semée depuis l'instantané — exactement la convergence que la colonne origin d'EventTypeAction existe pour garantir. Le SQL porte le corpus ; ce qui s'en déduit reste déduit.

Il n'efface rien

Aucun DELETE … NOT IN (…) de convergence. Il serait plus strict que l'import lui-même, dont le contrat écrit est « crée et met à jour, jamais ne retire ». Un tel DELETE emporterait un jeu de données créé depuis l'écran Datasets ou un message type ajouté à la main, les catalogues étant éditables. Contrepartie assumée, la même que côté corpus : un message type retiré du corpus subsiste sur une base déjà semée.

Qui charge l'instantané​

  • pnpm db:seed, dès que _source-oldproject/ est absent. La bascule est automatique et annoncée : « Corpus absent de … — chargement de l'instantané SQL ». Le rapport final est alors différent de celui de l'import : la plupart de ses compteurs mesurent l'assemblage, travail que l'instantané a déjà figé, et les afficher à zéro ferait lire « aucun gabarit réécrit » là où il faut lire « la question ne se pose pas ici ».
  • Le conteneur migrate des quatre fichiers compose (local, dev, preprod, prod). Il pose le schéma, puis lance le même import-source.ts — qui se replie, le corpus étant hors image — avant les autres semis. Les backends attendent qu'il ait réussi.
  • Un poste sans Python, ou fraîchement cloné, par la même bascule.

Le chargement passe par pg et non par Prisma : $executeRawUnsafe n'accepte qu'une instruction à la fois, là où un fichier en porte des milliers, et le protocole simple de PostgreSQL avale un lot entier d'un coup. Il se fait en une transaction — un fichier qui échouerait à mi-parcours laisserait sinon une base avec la moitié des technologies. Avant de valider, les comptes observés sont confrontés à ceux du manifeste : un instantané régénéré sans que son manifeste le soit partirait autrement incomplet, en silence.

Quand la rejouer​

Après un rafraîchissement de _source-oldproject/, évidemment. Mais aussi — et c'est le cas qu'on oublie — après toute modification des règles d'assemblage ou de réécriture des gabarits (template-rewrites.ts, template-assembly.ts). Ces règles sont cuites dans le SQL : sur le chemin de repli, elles ne sont pas rejouées. Une correction non suivie d'un pnpm db:snapshot laisse donc la CI et les environnements déployés sur l'ancien rendu.

C'est pourquoi un test le relève désormais. manifest.json porte templateDigest, le SHA-256 des cinq modules qui façonnent le texte d'un gabarit — template-rewrites.ts, template-assembly.ts, excluded-event-types.ts, source-registry.ts et extract-source-generators.py — et snapshot-digest.spec.ts le recalcule. Retoucher l'un d'eux, commentaire compris, fait rougir la CI jusqu'à la régénération. Le code d'import des pools et des populations en est exclu à dessein : il voisine avec des passes qui tournent sur les deux chemins, et un garde-fou qui sonne pour rien est un garde-fou qu'on cesse d'écouter.

pnpm db:snapshot # importe le corpus, puis régénère prisma/seed/corpus/
git add packages/database/prisma/seed/corpus/

L'instantané est versionné en clair, un fichier par technologie, précisément pour que le diff se relise : il montre quels messages types et quels gabarits ont changé. Relisez-le avant de committer — c'est ce que sèmeront la CI et les environnements déployés.

Quand ça coince​

SymptômeCauseGeste
« Corpus source introuvable »_source-oldproject/ absentLe déposer à la racine, ou passer son chemin en argument
DATABASE_URL manquantePas de .env à la racinecp .env.example .env
python3: command not foundLe sondage exécute la source du corpusInstaller Python 3
« La base ne porte aucune technologie : rien à photographier »L'import a échoué, ou la base a été vidée entre les deux gestesRelancer la commande entière, jamais l'export seul
« Cycle de rattachement entre technologies »Deux technologies se rattachent mutuellement à leurs poolsLe corpus est insoluble en l'état : le cycle se casse à la source
Au chargement : « L'instantané n'a pas produit ce que son manifeste annonce »Fichiers et manifeste désaccordésRégénérer l'instantané entier, jamais un fichier seul
snapshot-digest.spec.ts rouge : « …ont changé sans pnpm db:snapshot »Une règle de gabarit a été modifiée sans régénérer l'instantanépnpm db:snapshot, relire le diff, committer corpus/ avec elle