@nodefony/drizzle 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +162 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +105 -0
  6. package/dist/nodefony/command/migrateShared.js +247 -0
  7. package/dist/nodefony/command/orm-generate.js +356 -0
  8. package/dist/nodefony/command/orm-migrate-baseline.js +208 -0
  9. package/dist/nodefony/command/orm-migrate-repair.js +114 -0
  10. package/dist/nodefony/command/orm-migrate-status.js +67 -0
  11. package/dist/nodefony/command/orm-migrate.js +141 -0
  12. package/dist/nodefony/command/orm-reset.js +166 -0
  13. package/dist/nodefony/config/config.js +107 -0
  14. package/dist/nodefony/config/defineModuleConfig.js +63 -0
  15. package/dist/nodefony/entity/auditEventEntity.js +93 -0
  16. package/dist/nodefony/entity/colKit.js +260 -0
  17. package/dist/nodefony/entity/idempotencyEntity.js +74 -0
  18. package/dist/nodefony/entity/sessionEntity.js +75 -0
  19. package/dist/nodefony/entity/tokenEntity.js +198 -0
  20. package/dist/nodefony/entity/totpSecretEntity.js +98 -0
  21. package/dist/nodefony/entity/userTable.js +141 -0
  22. package/dist/nodefony/entity/webAuthnCredentialEntity.js +114 -0
  23. package/dist/nodefony/entity/webhookEndpointEntity.js +106 -0
  24. package/dist/nodefony/interfaces/IDrizzleConfig.js +1 -0
  25. package/dist/nodefony/interfaces/index.js +1 -0
  26. package/dist/nodefony/migrations-schema/mysql.js +48 -0
  27. package/dist/nodefony/migrations-schema/postgres.js +48 -0
  28. package/dist/nodefony/migrations-schema/sqlite.js +48 -0
  29. package/dist/nodefony/registerStores.js +218 -0
  30. package/dist/nodefony/service/DrizzleService.js +282 -0
  31. package/dist/nodefony/src/DrizzleAuditStore.js +203 -0
  32. package/dist/nodefony/src/DrizzleIdempotencyStore.js +278 -0
  33. package/dist/nodefony/src/DrizzleTokenStore.js +244 -0
  34. package/dist/nodefony/src/DrizzleTotpSecretStore.js +151 -0
  35. package/dist/nodefony/src/DrizzleUserRepository.js +217 -0
  36. package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +159 -0
  37. package/dist/nodefony/src/DrizzleWebhookStore.js +169 -0
  38. package/dist/nodefony/src/SessionStorage.js +259 -0
  39. package/dist/nodefony/src/connectorTarget.js +59 -0
  40. package/dist/nodefony/src/likeSql.js +50 -0
  41. package/dist/nodefony/src/migrator/DrizzleMigrator.js +775 -0
  42. package/dist/nodefony/src/migrator/adopt.js +553 -0
  43. package/dist/nodefony/src/migrator/appSchema.js +414 -0
  44. package/dist/nodefony/src/migrator/catalog.js +76 -0
  45. package/dist/nodefony/src/migrator/destructive.js +213 -0
  46. package/dist/nodefony/src/migrator/divergence.js +84 -0
  47. package/dist/nodefony/src/migrator/drivers/index.js +39 -0
  48. package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +147 -0
  49. package/dist/nodefony/src/migrator/drivers/postgresDriver.js +151 -0
  50. package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +121 -0
  51. package/dist/nodefony/src/migrator/explain.js +565 -0
  52. package/dist/nodefony/src/migrator/hash.js +47 -0
  53. package/dist/nodefony/src/migrator/history.js +219 -0
  54. package/dist/nodefony/src/migrator/index.js +16 -0
  55. package/dist/nodefony/src/migrator/kit.js +296 -0
  56. package/dist/nodefony/src/migrator/name.js +68 -0
  57. package/dist/nodefony/src/migrator/paths.js +88 -0
  58. package/dist/nodefony/src/migrator/refusals.js +143 -0
  59. package/dist/nodefony/src/migrator/resolve.js +281 -0
  60. package/dist/nodefony/src/migrator/schemaDiff.js +86 -0
  61. package/dist/nodefony/src/migrator/sources.js +419 -0
  62. package/dist/nodefony/src/migrator/status.js +231 -0
  63. package/dist/nodefony/src/migrator/types.js +91 -0
  64. package/dist/nodefony/src/orm-core/DrizzleOrm.js +1154 -0
  65. package/dist/nodefony/src/orm-core/DrizzleRepository.js +610 -0
  66. package/dist/nodefony/src/orm-core/DrizzleTransaction.js +106 -0
  67. package/dist/nodefony/src/orm-core/index.js +4 -0
  68. package/dist/nodefony/src/queryKit.js +318 -0
  69. package/dist/nodefony/src/safeTarget.js +55 -0
  70. package/dist/types/index.d.ts +76 -0
  71. package/dist/types/nodefony/command/migrateShared.d.ts +137 -0
  72. package/dist/types/nodefony/command/orm-generate.d.ts +53 -0
  73. package/dist/types/nodefony/command/orm-migrate-baseline.d.ts +87 -0
  74. package/dist/types/nodefony/command/orm-migrate-repair.d.ts +66 -0
  75. package/dist/types/nodefony/command/orm-migrate-status.d.ts +38 -0
  76. package/dist/types/nodefony/command/orm-migrate.d.ts +65 -0
  77. package/dist/types/nodefony/command/orm-reset.d.ts +55 -0
  78. package/dist/types/nodefony/config/config.d.ts +110 -0
  79. package/dist/types/nodefony/config/defineModuleConfig.d.ts +24 -0
  80. package/dist/types/nodefony/entity/auditEventEntity.d.ts +56 -0
  81. package/dist/types/nodefony/entity/colKit.d.ts +130 -0
  82. package/dist/types/nodefony/entity/idempotencyEntity.d.ts +52 -0
  83. package/dist/types/nodefony/entity/sessionEntity.d.ts +50 -0
  84. package/dist/types/nodefony/entity/tokenEntity.d.ts +53 -0
  85. package/dist/types/nodefony/entity/totpSecretEntity.d.ts +61 -0
  86. package/dist/types/nodefony/entity/userTable.d.ts +65 -0
  87. package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +57 -0
  88. package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +58 -0
  89. package/dist/types/nodefony/interfaces/IDrizzleConfig.d.ts +17 -0
  90. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  91. package/dist/types/nodefony/migrations-schema/mysql.d.ts +9 -0
  92. package/dist/types/nodefony/migrations-schema/postgres.d.ts +9 -0
  93. package/dist/types/nodefony/migrations-schema/sqlite.d.ts +9 -0
  94. package/dist/types/nodefony/registerStores.d.ts +52 -0
  95. package/dist/types/nodefony/service/DrizzleService.d.ts +27 -0
  96. package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +65 -0
  97. package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +129 -0
  98. package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +146 -0
  99. package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +60 -0
  100. package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +67 -0
  101. package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +62 -0
  102. package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +79 -0
  103. package/dist/types/nodefony/src/SessionStorage.d.ts +72 -0
  104. package/dist/types/nodefony/src/connectorTarget.d.ts +48 -0
  105. package/dist/types/nodefony/src/likeSql.d.ts +29 -0
  106. package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +94 -0
  107. package/dist/types/nodefony/src/migrator/adopt.d.ts +280 -0
  108. package/dist/types/nodefony/src/migrator/appSchema.d.ts +223 -0
  109. package/dist/types/nodefony/src/migrator/catalog.d.ts +100 -0
  110. package/dist/types/nodefony/src/migrator/destructive.d.ts +123 -0
  111. package/dist/types/nodefony/src/migrator/divergence.d.ts +61 -0
  112. package/dist/types/nodefony/src/migrator/drivers/index.d.ts +26 -0
  113. package/dist/types/nodefony/src/migrator/drivers/mysqlDriver.d.ts +83 -0
  114. package/dist/types/nodefony/src/migrator/drivers/postgresDriver.d.ts +81 -0
  115. package/dist/types/nodefony/src/migrator/drivers/sqliteDriver.d.ts +53 -0
  116. package/dist/types/nodefony/src/migrator/explain.d.ts +424 -0
  117. package/dist/types/nodefony/src/migrator/hash.d.ts +39 -0
  118. package/dist/types/nodefony/src/migrator/history.d.ts +139 -0
  119. package/dist/types/nodefony/src/migrator/index.d.ts +20 -0
  120. package/dist/types/nodefony/src/migrator/kit.d.ts +141 -0
  121. package/dist/types/nodefony/src/migrator/name.d.ts +52 -0
  122. package/dist/types/nodefony/src/migrator/paths.d.ts +54 -0
  123. package/dist/types/nodefony/src/migrator/refusals.d.ts +170 -0
  124. package/dist/types/nodefony/src/migrator/resolve.d.ts +201 -0
  125. package/dist/types/nodefony/src/migrator/schemaDiff.d.ts +112 -0
  126. package/dist/types/nodefony/src/migrator/sources.d.ts +119 -0
  127. package/dist/types/nodefony/src/migrator/status.d.ts +132 -0
  128. package/dist/types/nodefony/src/migrator/types.d.ts +273 -0
  129. package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +241 -0
  130. package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +98 -0
  131. package/dist/types/nodefony/src/orm-core/DrizzleTransaction.d.ts +71 -0
  132. package/dist/types/nodefony/src/orm-core/index.d.ts +11 -0
  133. package/dist/types/nodefony/src/queryKit.d.ts +136 -0
  134. package/dist/types/nodefony/src/safeTarget.d.ts +42 -0
  135. package/docs/index.md +954 -0
  136. package/docs/migrations.md +691 -0
  137. package/migrations/mysql/0000_framework_init.sql +137 -0
  138. package/migrations/mysql/meta/_journal.json +13 -0
  139. package/migrations/postgres/0000_framework_init.sql +128 -0
  140. package/migrations/postgres/meta/_journal.json +13 -0
  141. package/migrations/sqlite/0000_framework_init.sql +127 -0
  142. package/migrations/sqlite/meta/_journal.json +13 -0
  143. package/package.json +126 -0
@@ -0,0 +1,691 @@
1
+ ---
2
+ title: "Migrations de schéma — de la base de dev au déploiement sans interruption"
3
+ navTitle: Migrations de schéma
4
+ lang: fr
5
+ module: "@nodefony/drizzle"
6
+ topic: drizzle
7
+ section: "Persistance"
8
+ audience: [developer, devops]
9
+ tags:
10
+ [
11
+ migrations,
12
+ schema,
13
+ ddl,
14
+ deploiement,
15
+ kubernetes,
16
+ production,
17
+ sqlite,
18
+ postgresql,
19
+ mysql,
20
+ ]
21
+ version: "doc"
22
+ status: stable
23
+ updated: 2026-08-30
24
+ source: "src/packages/@nodefony/drizzle/nodefony/src/migrator/"
25
+ ---
26
+
27
+ 📍 [Documentation](../../../../../docs/index.md) › [Drizzle — ORM SQL](index.md) › **Migrations de schéma**
28
+
29
+ > Une base de données ne se met pas à jour toute seule, et elle ne se remplace pas non plus. Une
30
+ > **migration** est un fichier de SQL versionné qui la fait passer d'une version du schéma à la
31
+ > suivante, en gardant la trace de son passage. Cette page dit comment Nodefony les produit, les
32
+ > applique et les surveille — et surtout comment déployer sans interrompre le service, ce qui est la
33
+ > seule question difficile du sujet.
34
+ >
35
+ > Deux lecteurs, deux moitiés. **En développement**, presque rien à faire : le schéma se répare tout
36
+ > seul, et la seule commande à retenir est `orm:reset`. **En exploitation**, tout se joue avant le
37
+ > déploiement : un travail d'orchestrateur applique les migrations pendant que l'ancienne version
38
+ > sert encore, avec un compte qui n'est pas celui du trafic.
39
+
40
+ ## 📖 Lexique
41
+
42
+ | Terme | Ce que ça veut dire |
43
+ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | **migration** | un fichier de SQL versionné qui fait passer la base d'une version du schéma à la suivante, et qui garde trace de son passage |
45
+ | **schéma** | la forme de la base : ses tables, ses colonnes, leurs types et leurs contraintes — pas les données qu'elle contient |
46
+ | **DDL** | _Data Definition Language_ — la part du SQL qui crée et modifie le schéma (`CREATE TABLE`, `ALTER TABLE`), par opposition à celle qui lit et écrit les données |
47
+ | **source de migrations** | un dossier de migrations livré par quelqu'un : le framework en a une, l'application une autre, un module tiers peut en apporter une |
48
+ | **historique** | la table `nodefony_migrations` où l'applicateur écrit ce qu'il a posé, quand, et avec quelle empreinte |
49
+ | **empreinte** | la somme de contrôle du contenu d'un fichier de migration — c'est elle qui détecte qu'un fichier déjà appliqué a été modifié après coup |
50
+ | **adoption (_baseline_)** | déclarer qu'une base existante correspond déjà à certaines migrations, sans les exécuter |
51
+ | **dérive** | un fichier de migration modifié APRÈS avoir été appliqué : l'historique et le disque ne disent plus la même chose |
52
+ | **divergence** | la base ne correspond plus au code, alors que l'historique est complet et que rien n'est en attente |
53
+ | **expansion / contraction** | la façon de changer un schéma en deux déploiements, pour qu'à aucun moment le code en service et la base ne soient incompatibles |
54
+
55
+ ## Qu'est-ce qu'une migration, et pourquoi le développement n'en a pas besoin
56
+
57
+ En développement, Nodefony **dérive** le schéma du code : les entités que vous déclarez deviennent
58
+ des `CREATE TABLE IF NOT EXISTS` au démarrage. C'est immédiat et sans cérémonie — mais `IF NOT
59
+ EXISTS` ne fait évoluer aucune table qui existe déjà. Ajoutez un champ à une entité, et la table
60
+ gardera la forme qu'elle avait.
61
+
62
+ En production, cette dérivation serait pire qu'inutile : elle poserait des tables dont l'historique
63
+ ne garderait aucune trace, et personne ne saurait plus d'où elles viennent — exactement la
64
+ divergence que les migrations existent pour empêcher.
65
+
66
+ D'où **trois modes**, un par connecteur, réglés par la clé `ddl` :
67
+
68
+ | Mode | Qui fabrique le schéma | Où c'est le défaut |
69
+ | --------- | ------------------------------------------------------------ | ---------------------------------- |
70
+ | `auto` | dérivé du code au démarrage, et **rattrapé** (voir plus bas) | développement, test |
71
+ | `migrate` | les migrations, appliquées au démarrage sous verrou | nulle part — jamais un défaut |
72
+ | `none` | personne ici : un travail extérieur s'en charge | tout le reste, production comprise |
73
+
74
+ > [!IMPORTANT]
75
+ > `migrate` n'est le défaut d'aucun environnement, et c'est délibéré. Faire migrer le schéma par le
76
+ > processus qui sert le trafic, c'est accepter que N exemplaires démarrant ensemble se disputent la
77
+ > base. Le verrou les sérialise, mais le patron sain reste le travail d'orchestrateur décrit plus
78
+ > bas. `migrate` existe pour les déploiements sans orchestrateur — un serveur unique, une machine
79
+ > virtuelle.
80
+
81
+ ## 🚀 Démarrage rapide
82
+
83
+ Cinq verbes, et un seul à retenir pour le quotidien du développement.
84
+
85
+ ```bash
86
+ # Ce que la base a reçu, ce qui reste, et ce qu'il faut taper. N'écrit rien.
87
+ nodefony orm:migrate:status
88
+
89
+ # Applique ce qui est en attente (framework d'abord, application ensuite).
90
+ nodefony orm:migrate
91
+
92
+ # Écrire la migration qui aligne la base sur VOS entités.
93
+ nodefony orm:generate --name ajout_du_titre
94
+
95
+ # Voir le SQL sans l'appliquer — la même validation que la vraie.
96
+ nodefony orm:migrate --dry-run
97
+
98
+ # Adopter une base existante : marquer des migrations comme appliquées, sans les exécuter.
99
+ nodefony orm:migrate:baseline
100
+
101
+ # Reprendre une base qui existait AVANT toute migration : la référence est LUE sur la base.
102
+ nodefony orm:migrate:baseline --from-database
103
+
104
+ # Effacer les marqueurs d'échec, APRÈS avoir regardé ce qui s'est passé.
105
+ nodefony orm:migrate:repair
106
+
107
+ # Développement seulement : supprime et recrée la base du connecteur.
108
+ nodefony orm:reset
109
+ ```
110
+
111
+ Toutes acceptent `--connector <nom>` (défaut : `default`) et `--json`. Le flux `--json` est **pur** :
112
+ `nodefony orm:migrate:status --json | jq` ne casse sur aucune ligne de journal.
113
+
114
+ Les migrations du **framework** sont livrées dans le paquet : vous n'avez pas à les produire. Celles
115
+ de votre **application**, vous les écrivez avec `orm:generate` — voir la section suivante.
116
+
117
+ Côté configuration, il n'y a rien à écrire pour le cas courant : le mode se résout par
118
+ environnement. Ne le déclarer que pour s'en écarter — un serveur unique qui migre au démarrage :
119
+
120
+ ```typescript
121
+ import { defineConfig, use } from "nodefony";
122
+
123
+ export default defineConfig(() => ({
124
+ modules: [
125
+ use("@nodefony/drizzle", {
126
+ connectors: {
127
+ // Le démarrage applique les migrations, sous verrou. À réserver aux
128
+ // déploiements SANS orchestrateur — un serveur unique, une machine
129
+ // virtuelle : ailleurs, c'est un travail dédié qui migre, et les
130
+ // exemplaires restent en "none".
131
+ default: { ddl: "migrate" },
132
+ },
133
+ migrations: {
134
+ // Retenir la mise en service tant que le schéma est en retard (défaut),
135
+ // et faire de la divergence une barrière plutôt qu'une observation.
136
+ check: "fail",
137
+ divergence: "fail",
138
+ },
139
+ }),
140
+ ],
141
+ }));
142
+ ```
143
+
144
+ ## Écrire les migrations de votre application — `orm:generate`
145
+
146
+ Vous modifiez une entité, vous tapez un verbe, vous relisez le fichier produit :
147
+
148
+ ```bash
149
+ nodefony orm:generate --name ajout_du_titre
150
+ ```
151
+
152
+ Il n'y a **rien à installer ni à configurer**. La commande trouve vos entités là où le générateur
153
+ les écrit — `nodefony/entity/*.ts`, dans l'application et dans chacun de ses modules —, produit la
154
+ migration dans `migrations/<dialecte>/`, et vous dit ce qu'elle a écrit. L'outil qui calcule la
155
+ différence est piloté à l'intérieur ; vous n'avez ni sa configuration à tenir, ni son dossier de
156
+ sortie à connaître, ni son journal à comprendre.
157
+
158
+ Le dialecte est celui de votre connecteur : vos entités sont du Drizzle **natif**, donc écrites pour
159
+ un moteur. C'est ce qui vous laisse toute la puissance du moteur dans une entité — et ce qui fait
160
+ qu'une migration vaut pour lui seul.
161
+
162
+ ### Ce que la commande refuse, et pourquoi c'est une bonne nouvelle
163
+
164
+ **Une migration est immuable dès qu'une base l'a reçue.** Une migration à laquelle il manque une
165
+ table ne se corrige donc pas : elle se remplace par une suivante, sur toutes les bases qui ont déjà
166
+ appliqué la première. C'est pour cela que trois situations arrêtent la commande avant qu'elle
167
+ n'écrive quoi que ce soit — chacune nomme ce qui cloche :
168
+
169
+ | Ce qui est refusé | Ce que ça veut dire |
170
+ | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
171
+ | une entité enregistrée qu'aucun fichier ne fournit | son fichier a été déplacé, renommé, ou ne s'importe pas seul — la migration serait écrite **sans sa table** |
172
+ | un fichier de l'application qui exporte une table du framework | la migration porterait un second `CREATE TABLE` pour cette table : elle passerait sur une base vierge, et échouerait sur toute base **déjà migrée** |
173
+ | une migration qui **supprime** des données | les fichiers **sont écrits** et conservés : ce sont eux qu'il faut lire. C'est leur mise en service qui est retenue, et elle a sa propre garde |
174
+
175
+ Le troisième cas mérite deux précisions.
176
+
177
+ D'abord, quand une colonne disparaît et qu'une autre apparaît, aucun outil ne peut deviner s'il
178
+ s'agit d'un **renommage** — les données suivent — ou d'une suppression suivie d'un ajout — les
179
+ données sont perdues. C'est une intention, pas une différence de schéma. Rejouez alors la commande
180
+ dans un terminal, et répondez à la question posée.
181
+
182
+ Ensuite, **il n'y a rien à regénérer pour « assumer »** : au moment où ce refus paraît, les
183
+ fichiers sont déjà écrits et inscrits au journal. Relancer la génération ne produirait plus rien —
184
+ elle répondrait « le schéma n'a pas bougé ». Les deux issues réelles sont donc celles que le refus
185
+ propose : annuler les fichiers avec votre outil de gestion de versions, ou les appliquer. C'est
186
+ `orm:migrate` qui met en service, et c'est lui qui porte la garde `--allow-destructive` hors du
187
+ développement.
188
+
189
+ Un quatrième refus n'a rien à voir avec votre schéma : **l'outil qui écrit les migrations n'est pas
190
+ installé** (`NF_GENERATE_TOOL_MISSING`). C'est une dépendance de DÉVELOPPEMENT — votre application
191
+ la déclare, et un `npm install` la pose. Elle manque quand l'installation s'est faite sans les
192
+ dépendances de développement (`--omit=dev`, une image de production). Le refus le dit et donne le
193
+ geste ; il ne parle pas de votre base, qui n'y est pour rien — appliquer des migrations, lui, ne
194
+ réclame aucun outil tiers.
195
+
196
+ ### Une entité qui pointe vers une table du framework
197
+
198
+ Déclarer une référence vers une table du framework (`session`, `audit_event`…) est légitime : ce
199
+ qui est refusé, c'est de **ré-exporter** cette table depuis vos fichiers. Les tables du framework
200
+ sont exclues du plan de votre application — elles ont leurs propres migrations, appliquées avant les
201
+ vôtres. Pour une vraie clé étrangère SQL, écrivez une migration libre.
202
+
203
+ > [!IMPORTANT]
204
+ > **`User` n'en fait PAS partie.** L'identité est du domaine : la table `User` appartient à votre
205
+ > application, qui la décrit et en porte les migrations. Le framework ne la livre plus. C'est ce
206
+ > qui vous permet d'y ajouter vos propres champs — ce qu'aucune table du framework n'autorise.
207
+
208
+ ### Ce qu'aucun schéma ne peut déduire — la migration libre
209
+
210
+ Une vue, un déclencheur, un index particulier, un remplissage de données ne se déduisent d'aucune
211
+ entité :
212
+
213
+ ```bash
214
+ nodefony orm:generate --custom --name vue_des_ventes
215
+ ```
216
+
217
+ Vous obtenez un fichier **vide**, déjà inscrit au journal, que vous écrivez à la main. Il est
218
+ appliqué comme les autres : une seule fois, dans l'ordre, et son empreinte est gravée.
219
+
220
+ ### Reprendre une base qui existait AVANT toute migration
221
+
222
+ C'est l'état d'une application qui passe du développement à la production : la base porte ses
223
+ tables **et** ses données, et le dossier `migrations/` est vide — le mode de développement les a
224
+ créées au démarrage, sans jamais écrire de fichier.
225
+
226
+ Dans cet état, `orm:generate` **refuse** (`NF_GENERATE_DATABASE_NOT_ADOPTED`). Ce n'est pas une
227
+ précaution : la première migration décrirait la _création_ de tables qui existent, elle ne
228
+ s'appliquerait jamais, et l'adopter graverait dans l'historique un schéma que la base n'a pas —
229
+ l'état dont plus aucune commande ne sort.
230
+
231
+ Le refus nomme le geste, et **seulement celui qui produira quelque chose**. Si la base porte
232
+ _toutes_ les tables que le code déclare — le cas quand rien n'a changé depuis qu'elle a été
233
+ faite — il n'y a **qu'une** commande, et elle EST votre première migration :
234
+
235
+ ```bash
236
+ nodefony orm:migrate:baseline --from-database
237
+ ```
238
+
239
+ Regénérer derrière ne donnerait rien : le code n'a pas bougé, il n'y a aucun écart à écrire.
240
+ La suite redevient ordinaire — le jour où vous ajoutez un champ :
241
+
242
+ ```bash
243
+ nodefony orm:generate --name ajout_du_slug # produit un ALTER, plus un CREATE
244
+ nodefony orm:migrate
245
+ ```
246
+
247
+ Si la base n'en porte qu'une _partie_ — des entités ont été ajoutées depuis —, le refus propose
248
+ les deux gestes : l'adoption d'abord, la génération ensuite, qui écrira l'écart restant.
249
+
250
+ La commande **lit** le schéma de la base, en écrit la migration de référence sous
251
+ `migrations/<dialecte>/0000_<nom>.sql`, et l'inscrit comme appliquée. **Aucune instruction n'est
252
+ exécutée sur la base** : elle décrit un état déjà atteint. Son corps est laissé _exécutable_, pour
253
+ qu'un environnement neuf puisse être monté depuis ces mêmes fichiers.
254
+
255
+ Deux faits qu'elle publie, et qu'il faut lire :
256
+
257
+ | Ce qu'elle dit | Ce que ça veut dire |
258
+ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
259
+ | des tables **lues sans être déclarées** | la base est partagée. L'outil de lecture ne sait pas restreindre son champ ; la génération suivante proposera de les **supprimer**. Relisez le fichier. |
260
+ | un corps **resté en commentaire** | la référence ne recréerait rien sur une base neuve — l'outil a changé sa mise en forme. |
261
+
262
+ Deux nettoyages ont lieu sans que vous ayez à y penser, et il vaut mieux savoir qu'ils existent :
263
+
264
+ - **Les objets d'une table exclue ne sont pas repris.** L'exclusion porte sur les tables, jamais sur
265
+ ce qui gravite autour : la séquence de la table d'historique entrait dans la référence, qui
266
+ échouait alors sur un environnement neuf — la séquence y est déjà posée par les migrations du
267
+ framework — et dont la génération suivante proposait la suppression, que la base refuse.
268
+ - **PostgreSQL : le schéma visé est celui de votre connexion, et il est retiré de la référence.**
269
+ Si votre application vit ailleurs que dans `public` (le montage habituel d'une base mutualisée),
270
+ la lecture se fait bien dans VOTRE schéma — sans quoi elle décrirait les tables de quelqu'un
271
+ d'autre. Mais la référence, elle, est écrite sans le nommer : vos entités ne le nomment pas non
272
+ plus, et une référence qualifiée ferait poser une question de renommage au premier champ ajouté.
273
+
274
+ > ⚠️ **Sur MariaDB, cette commande refuse — et elle dit pourquoi.** MariaDB n'a pas de type JSON
275
+ > natif : il l'écrit en `longtext` assorti d'un `CHECK (json_valid(…))`. L'outil qui relit les
276
+ > schémas ne sait pas lire ces contraintes, et il lit la base **entière** avant de filtrer : les
277
+ > tables du framework suffisent donc à le bloquer. Le repli tient en quatre gestes, que le message
278
+ > d'erreur rappelle — relever le schéma (`SHOW CREATE TABLE`), un `orm:generate --custom`, y coller
279
+ > le schéma, puis `orm:migrate:baseline`.
280
+ >
281
+ > **Cela ne concerne que cette commande de reprise.** Créer les tables, appliquer les migrations et
282
+ > faire tourner l'application sont inchangés sur MariaDB.
283
+
284
+ ## En développement — le schéma se répare tout seul
285
+
286
+ C'est le scénario de tous les jours d'une équipe : le back ajoute un champ à une entité, le front
287
+ tire la branche, et sa base locale date d'avant.
288
+
289
+ Au démarrage, en mode `auto`, la connexion se fait en **trois temps** — et l'ordre est ce qui compte :
290
+
291
+ 1. les tables manquantes sont créées (`CREATE TABLE IF NOT EXISTS`) ;
292
+ 2. le schéma déclaré est **comparé** à celui de la base, table par table
293
+ (`compareSchema()`, `schemaDiff.ts:122`) ;
294
+ 3. les index sont posés.
295
+
296
+ Entre les deux, ce qui se rattrape est rattrapé :
297
+
298
+ - **colonne manquante qui accepte le vide** → elle est ajoutée (`additiveSql()`,
299
+ `schemaDiff.ts:181`) et journalisée en clair. Le front n'a rien à taper : il tire, le serveur
300
+ redémarre, ça marche.
301
+ - **colonne manquante et obligatoire** → jamais posée. La créer exigerait d'inventer une valeur pour
302
+ les lignes déjà présentes, ce qui est une décision métier, pas une décision d'outil. L'écart est
303
+ publié, journalisé, avec le geste exact.
304
+ - **colonne en trop dans la base** → ignorée, toujours. Une application qui écrit des migrations
305
+ libres (une vue, un déclencheur, une colonne ajoutée à une table d'entité) a une base
306
+ légitimement différente du schéma déclaré, en permanence.
307
+
308
+ > [!NOTE]
309
+ > Ce troisième temps n'est pas cosmétique. Un index porte sur des colonnes : le poser sur une table
310
+ > à laquelle il en manque une échoue, et cet échec-là **tuait le démarrage** — le développeur
311
+ > recevait un `no such column` du pilote, sans nom de connecteur, sans geste, et sans serveur pour
312
+ > aller voir. Les index d'une table encore en écart sont donc sautés : l'écart, lui, a déjà été dit.
313
+
314
+ Quand le rattrapage ne suffit pas, une seule commande à retenir :
315
+
316
+ ```bash
317
+ nodefony orm:reset --connector default
318
+ ```
319
+
320
+ Elle **supprime et recrée** la base, et refuse dès que l'environnement n'est pas `development` —
321
+ liste blanche, pas liste noire : `staging` et tout environnement inconnu refusent aussi.
322
+
323
+ ## En production — le patron de déploiement
324
+
325
+ ### Le travail d'orchestrateur, AVANT le déploiement des pods
326
+
327
+ Le consensus cloud-native est solide, et Nodefony ne cherche pas à le remplacer : **les migrations
328
+ s'appliquent dans un travail dédié, qui se termine avant que le premier nouvel exemplaire ne
329
+ démarre**. Les pods, eux, tournent en `ddl: "none"`.
330
+
331
+ ```yaml
332
+ # Kubernetes — le patron de référence. Le déploiement attend la fin du travail.
333
+ apiVersion: batch/v1
334
+ kind: Job
335
+ metadata:
336
+ name: nodefony-migrate
337
+ spec:
338
+ backoffLimit: 2
339
+ template:
340
+ spec:
341
+ restartPolicy: Never
342
+ containers:
343
+ - name: migrate
344
+ image: mon-application:1.4.0
345
+ command: ["npx", "nodefony", "orm:migrate", "--json"]
346
+ env:
347
+ - name: NF_MIGRATE_DATABASE_URL
348
+ valueFrom:
349
+ secretKeyRef: { name: db-migrator, key: url }
350
+ ```
351
+
352
+ Trois propriétés en découlent, et elles valent d'être nommées :
353
+
354
+ - **Le verrou rend les variantes sûres.** Si deux exemplaires du travail partent ensemble, le second
355
+ attend : le verrou est natif au moteur — `lock()` (`postgresDriver.ts:178`) demande un
356
+ `pg_advisory_lock` —, donc il s'auto-libère à la mort de la connexion. Une table de verrou maison laisserait un zombie à
357
+ déverrouiller à la main.
358
+ - **Rien n'est appliqué deux fois.** L'historique (`nodefony_migrations`, `types.ts:23`) est écrit
359
+ dans la même transaction que le DDL, là où le moteur le permet.
360
+ - **Un pod en retard ne reçoit pas de trafic.** En `none` comme en `migrate`, l'état du schéma est
361
+ publié à la sonde de disponibilité (`#publishReadiness()`, `DrizzleService.ts:345`) : `/readyz`
362
+ répond 503, l'orchestrateur sort l'exemplaire du répartiteur de charge, et l'ancien continue de
363
+ servir. `/livez` n'est jamais touché — un schéma en retard n'est pas un processus malade, et le
364
+ redémarrer ne réparerait rien. La vérification est rejouée toutes les 15 secondes : dès que le
365
+ schéma est à jour, l'exemplaire redevient disponible **tout seul**, sans redéploiement.
366
+
367
+ ### Les droits du compte qui migre ne sont pas ceux du trafic
368
+
369
+ Le compte qui applique une migration a besoin de modifier le schéma. Celui qui sert les requêtes,
370
+ non — et lui laisser ce pouvoir, c'est offrir un `DROP TABLE` à la première injection réussie.
371
+
372
+ `orm:migrate` lit donc `NF_MIGRATE_DATABASE_URL` **en priorité sur l'URL du connecteur**, et cette
373
+ variable n'est lue par personne d'autre : c'est le véhicule du moindre privilège.
374
+
375
+ ```sql
376
+ -- PostgreSQL : le compte du TRAFIC ne peut que lire et écrire des données.
377
+ GRANT CONNECT ON DATABASE app TO app_runtime;
378
+ GRANT USAGE ON SCHEMA public TO app_runtime;
379
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_runtime;
380
+
381
+ -- Le compte qui MIGRE possède le schéma, et ne sert jamais de requête applicative.
382
+ GRANT CREATE ON SCHEMA public TO app_migrator;
383
+ ALTER DEFAULT PRIVILEGES FOR ROLE app_migrator IN SCHEMA public
384
+ GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_runtime;
385
+ ```
386
+
387
+ > [!WARNING]
388
+ > `orm:reset` **n'est jamais** concerné : il ne lit pas `NF_MIGRATE_DATABASE_URL`. Une variable qui
389
+ > porte des droits de schéma ne doit pas pouvoir désigner la cible d'un effacement.
390
+
391
+ ### Déployer sans interruption — expansion, puis contraction
392
+
393
+ Pendant un déploiement progressif, deux versions du code parlent à **une seule** base. La règle qui
394
+ en découle n'a rien d'optionnel : **toute migration doit être compatible avec la version encore en
395
+ service**. Un changement destructif se fait donc en deux déploiements.
396
+
397
+ Renommer une colonne `email` en `contact_email`, sans une seconde d'interruption :
398
+
399
+ | Étape | Migration | Code déployé |
400
+ | --------------- | --------------------------------- | ------------------------------------ |
401
+ | 1 — expansion | ajouter `contact_email`, nullable | écrit les DEUX colonnes, lit `email` |
402
+ | 2 | recopier les données existantes | inchangé |
403
+ | 3 | — | lit `contact_email`, écrit les deux |
404
+ | 4 — contraction | supprimer `email` | ne connaît plus `email` |
405
+
406
+ Chaque étape est déployable seule, et à aucun moment la base n'est incompatible avec ce qui tourne.
407
+ C'est plus long que le `ALTER TABLE … RENAME` d'un seul coup — et c'est la seule façon connue de ne
408
+ pas couper le service.
409
+
410
+ ## Pourquoi il n'y a PAS de sauvegarde automatique
411
+
412
+ Aucune commande de Nodefony ne sauvegarde votre base avant d'agir. C'est une décision, pas un
413
+ manque, et **aucun outil de migration sérieux ne le fait** non plus.
414
+
415
+ Une sauvegarde automatique donnerait une assurance qui n'existe pas. Sur une base de production de
416
+ plusieurs centaines de gigaoctets, la prendre depuis le processus qui migre prendrait des heures,
417
+ saturerait le disque de l'exemplaire, et échouerait au pire moment. Sur une base répliquée, elle
418
+ ignorerait les réplicas. Et surtout : **une restauration est une décision d'exploitation**, avec sa
419
+ fenêtre d'indisponibilité, sa perte de données assumée entre deux points de reprise, et quelqu'un
420
+ qui la prend. Un outil qui prétendrait la préparer tout seul inviterait à ne pas y penser.
421
+
422
+ Ce que l'outil fait à la place, et qui vaut mieux :
423
+
424
+ - **il refuse d'appliquer sans que vous sachiez.** Le SQL en attente est examiné avant la moindre
425
+ écriture (`scanDestructive()`, `destructive.ts:184`) : une suppression de données est refusée hors
426
+ développement, en nommant l'instruction et ce qui disparaît. Il faut `--allow-destructive` pour
427
+ passer outre, et **au démarrage il n'y a aucun drapeau pour lever le refus** — un exemplaire qui
428
+ redémarre ne supprime jamais de colonne de lui-même, parce que personne ne regarde à ce
429
+ moment-là ;
430
+ - **il vous apprend l'expansion/contraction au moment où elle sert**, c'est-à-dire quand vous lisez
431
+ le refus.
432
+
433
+ La protection réelle contre la perte de données, c'est le patron ci-dessus et votre politique de
434
+ sauvegarde — pas un fichier `.bak` pris par un outil qui ne sait rien de votre infrastructure.
435
+
436
+ ## La troisième source — le verdict `divergent`
437
+
438
+ Les outils de migration connaissent **deux** choses : les fichiers, et l'historique. Ils en
439
+ concluent « tout est appliqué ». Ils ne regardent jamais la base.
440
+
441
+ Nodefony croise une **troisième** source (`describeDivergence()`, `divergence.ts:127`), et rend un
442
+ constat qu'aucun outil ne produit en continu :
443
+
444
+ > l'historique est complet, aucune migration n'est en attente — **et pourtant la base ne correspond
445
+ > pas au code**.
446
+
447
+ Quelqu'un a passé un `ALTER` à la main un soir d'astreinte, un correctif d'urgence n'a jamais été
448
+ reporté, deux environnements ont divergé. Le verdict s'appelle `divergent`, il s'affiche dans
449
+ `orm:migrate:status`, il est publié par la sonde, et il obéit à trois règles :
450
+
451
+ - **il ne se paie que lorsque les deux autres sources n'ont plus rien à dire.** Tant qu'une
452
+ migration attend, le verdict est déjà décidé : interroger la base coûterait une requête par table
453
+ sans rien apprendre ;
454
+ - **il signale ce qui MANQUE, jamais ce qu'il trouve en trop.** Sans cette règle, toute application
455
+ à migrations libres l'aurait allumé à vie — donc appris comme du bruit, donc mort ;
456
+ - **il ne fait pas tomber un déploiement — sauf quand une TABLE d'entité manque.** Le code de
457
+ sortie reste `0` pour une colonne en écart : superviser n'est pas bloquer, et une application à
458
+ migrations libres en a une en permanence. Mais aucune main légitime ne fait _disparaître_ une
459
+ table que le code déclare comme entité : quand elle manque, le schéma applicatif n'a jamais été
460
+ posé, l'application rendra 500 sur chacune de ses routes, et le processus **retient sa mise en
461
+ service** (`/readyz` répond 503). Le seuil se règle :
462
+
463
+ ```typescript
464
+ use("@nodefony/drizzle", {
465
+ // "report" (défaut) — affiche tout écart, ne retient QUE sur une table absente
466
+ // "fail" — tout écart retient la mise en service
467
+ // "off" — rien n'est comparé, rien n'est publié
468
+ migrations: { divergence: "fail" },
469
+ });
470
+ ```
471
+
472
+ **La graduation est unique** (`divergenceIsBlocking()`, `explain.ts`) : la commande et la sonde de
473
+ disponibilité lisent la même règle, donc votre passe d'intégration continue et votre orchestrateur
474
+ ne peuvent pas se contredire.
475
+
476
+ ### Il dit CE QUI diverge, pas seulement QU'IL diverge
477
+
478
+ Un verdict qui annonce un écart sans le nommer envoie ouvrir un client SQL et comparer table par
479
+ table, sur une base de production, au pire moment. La sortie porte donc les tables et les colonnes,
480
+ **séparées selon qu'elles se rattrapent ou non** — une colonne qui accepte le vide s'ajoute sans
481
+ rien inventer, une colonne obligatoire exige une décision métier :
482
+
483
+ ```bash
484
+ nodefony orm:migrate:status --json | jq '.divergence'
485
+ ```
486
+
487
+ ```json
488
+ {
489
+ "missingTables": ["webhook_endpoint"],
490
+ "blocking": [
491
+ { "table": "User", "column": "tenantId", "type": "text", "nullable": false }
492
+ ],
493
+ "additive": [
494
+ {
495
+ "table": "audit_event",
496
+ "column": "metadata",
497
+ "type": "jsonb",
498
+ "nullable": true
499
+ }
500
+ ]
501
+ }
502
+ ```
503
+
504
+ La clé vit au premier niveau, dans le cœur neutre — pas sous `driver` : un second ORM remplira la
505
+ même structure, et un `jq` écrit aujourd'hui ne doit pas graver le nom d'un pilote. **Sur une base
506
+ conforme, la clé est ABSENTE** (jamais un objet vide) : `.divergence == null` suffit à tester.
507
+
508
+ L'écran lisible en dit autant : le résumé nomme les trois premières entrées de chaque famille, et la
509
+ liste complète ne se déroule que lorsqu'elle ne tient plus dans la phrase.
510
+
511
+ Les gestes proposés suivent **l'environnement** : `orm:reset` efface, elle n'est acceptée qu'en
512
+ développement, et elle n'est donc proposée que là. Ailleurs, la sortie renvoie vers l'écriture d'une
513
+ migration correctrice (`orm:generate --custom`) puis son application.
514
+
515
+ Ils suivent aussi **ce qui manque**. Une table d'entité absente se rattrape par le générateur — il
516
+ sait la produire, puisque le code la déclare — et c'est `orm:generate --name …` qui est proposé, pas
517
+ `--custom` : envoyer écrire à la main ce que la commande d'à côté écrit seule serait un geste juste
518
+ pour une colonne et absurde pour une table.
519
+
520
+ ## Quand l'historique MENT — `repair --forget`
521
+
522
+ Il existe un état que ni les fichiers ni l'historique ne peuvent signaler seuls : **une migration
523
+ inscrite comme réussie que personne n'a jamais exécutée.** L'historique est complet, rien n'est en
524
+ attente, aucun marqueur d'échec — et pourtant la base ne porte pas les tables.
525
+
526
+ Deux chemins y mènent, tous deux réels :
527
+
528
+ - une **adoption bornée au mauvais endroit** : `orm:migrate:baseline --up-to <tag>` inscrit tout ce
529
+ qui précède le tag, et si l'on s'est trompé de borne, ce qui suit a été gravé sans être appliqué ;
530
+ - une base **héritée** d'une version antérieure aux gardes actuelles.
531
+
532
+ Le verdict `divergent` le voit et NOMME ce qui manque. Mais aucune commande ne « rejoue » une
533
+ migration que l'historique donne pour appliquée : la réparation ordinaire ne connaît que les
534
+ marqueurs d'ÉCHEC, et répond « rien à réparer ». C'est une impasse, et une impasse fait faire des
535
+ gestes qu'on regrette — au banc de découvrabilité, le seul chemin restant était d'effacer la base.
536
+
537
+ ```bash
538
+ nodefony orm:migrate:status --json # NOMME les tables qui manquent
539
+ nodefony orm:migrate:repair --forget app/0003_facture # désinscrit CETTE migration
540
+ nodefony orm:migrate # elle est rejouée
541
+ ```
542
+
543
+ Trois propriétés, et elles sont volontaires :
544
+
545
+ | Propriété | Pourquoi |
546
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
547
+ | **il faut la NOMMER** | `<source>/<tag>` exactement. Ni motif, ni lot, ni « toutes celles de cette source » : un oubli en masse est le geste qu'on ne veut pas rendre facile. |
548
+ | **la base n'est PAS touchée** | seule la ligne d'historique disparaît. Si la migration avait bien été appliquée, son rejeu échouera — bruyamment, et c'est le comportement voulu. |
549
+ | **c'est le produit qui le fait** | l'interdit de toucher à la table d'historique vise la main dans un client SQL, sans trace. Le geste existe donc ici, borné et journalisé, plutôt qu'ailleurs. |
550
+
551
+ > ⚠️ N'utilisez cette option que sur un état CONSTATÉ. Le point de départ n'est pas ce refus, c'est
552
+ > `orm:migrate:status` : c'est lui qui dit quelles tables manquent, donc quelle migration n'a
553
+ > jamais tourné.
554
+
555
+ ## Codes de sortie et sortie `--json`
556
+
557
+ La grille est **figée, et ne sera jamais réaffectée** — des passes d'intégration continue s'y
558
+ adossent, et en changer le sens casserait des tests que nous ne voyons pas.
559
+
560
+ | Code | Ce que ça veut dire |
561
+ | ---- | --------------------------------------------------------------------------------------------------------------- |
562
+ | `0` | à jour, ou appliqué avec succès |
563
+ | `1` | une action humaine est requise : migrations en attente, dérive, échec, refus destructif, table d'entité absente |
564
+ | `2` | la commande n'a pas pu travailler : base injoignable, verrou indisponible, usage invalide |
565
+
566
+ ```bash
567
+ # Barrière d'intégration continue : la passe s'arrête si le schéma n'est pas à jour.
568
+ nodefony orm:migrate:status --json || exit 1
569
+
570
+ # Ce qu'un agent lit — jamais une phrase française.
571
+ nodefony orm:migrate:status --json | jq -r '.verdict, .nextActions[0].command'
572
+ ```
573
+
574
+ Chaque sortie `--json` porte `formatVersion: 1` au premier niveau. Ajouter un champ est une version
575
+ mineure ; en retirer ou en renommer un est interdit sur la série majeure.
576
+
577
+ ## Le même état, dans la console d'administration
578
+
579
+ Ce que la ligne de commande rend, la console le montre — page **Migrations**
580
+ (`/nodefony/migrate`) : le verdict, l'identité du connecteur, les gestes à copier, et une ligne par
581
+ migration avec sa date, sa durée, son auteur, son déploiement et le motif de son échec.
582
+
583
+ Trois points du plan d'administration la servent, tous derrière le rôle d'administration :
584
+
585
+ | Point | Ce qu'il rend |
586
+ | ---------------------------------------------------- | ------------------------------------------------------------------- |
587
+ | `GET /nodefony/orm/api/migrations?connector=` | **exactement** la charge utile de `orm:migrate:status --json` |
588
+ | `GET /nodefony/orm/api/migrations/plan?connector=` | ce qui S'APPLIQUERAIT, avec le SQL de chaque migration en attente |
589
+ | `POST /nodefony/orm/api/migrations/apply?connector=` | applique — **refusé hors développement**, en disant par quoi passer |
590
+
591
+ Trois choses valent d'être dites, parce qu'elles décident de ce que vous pouvez croire à l'écran :
592
+
593
+ - **L'écran ne calcule rien.** Il affiche l'objet que le plan lui rend, et cet objet est celui de la
594
+ commande — vérifié par égalité. Deux calculs de la même question finiraient par se contredire, et
595
+ c'est le jour d'un incident qu'on s'en apercevrait.
596
+ - **Appliquer depuis la console n'existe qu'en développement**, et le refus vient du produit, pas de
597
+ l'interface : un appel direct au plan est refusé de la même façon. En production, les migrations
598
+ passent par un travail d'orchestrateur qui se termine AVANT que le premier nouvel exemplaire ne
599
+ démarre.
600
+ - **Un connecteur qui ne porte pas de migrations reçoit une réponse qui le NOMME** (`501`), jamais
601
+ une page vide. Un écran qui se tait quand la donnée manque ressemble à « tout va bien ».
602
+
603
+ ## ⚠️ Pièges
604
+
605
+ - **Un fichier de migration déjà appliqué ne se modifie pas.** L'empreinte le détecte et le verdict
606
+ devient `drift`. Écrivez une migration correctrice — c'est plus long, et c'est ce qui garde
607
+ l'historique honnête.
608
+ - **MySQL ne sait pas annuler un `CREATE TABLE`.** Son DDL valide implicitement : après un échec à
609
+ mi-course, la reprise aveugle est interdite, et c'est `orm:migrate:repair` — après inspection
610
+ humaine — qui tranche.
611
+ - **Une colonne obligatoire SANS valeur par défaut ne se comporte pas pareil selon le moteur.** Sur
612
+ une table qui porte déjà des lignes, sqlite refuse (« Cannot add a NOT NULL column with default
613
+ value NULL ») et PostgreSQL refuse (« contains null values ») — ils ne peuvent pas inventer la
614
+ valeur des lignes existantes. **MySQL/MariaDB accepte** et les remplit de chaînes vides, mode
615
+ strict compris : le champ est déclaré obligatoire et ne contient que du vide, sans un
616
+ avertissement. Donnez toujours un défaut, ou déclarez le champ facultatif ; si les deux sont
617
+ nécessaires, c'est trois migrations — ajouter avec défaut, remplir (`--custom`), retirer le
618
+ défaut. Mesuré sur les trois moteurs :
619
+ `src/packages/@nodefony/drizzle/tests/integration/user-migrations.e2e.test.ts:1`.
620
+ - **`orm:migrate:repair --update-hashes` réécrit les empreintes.** Il fait taire une dérive au lieu
621
+ de la corriger : les autres bases ont reçu l'ancienne version du fichier et ne recevront jamais la
622
+ nouvelle. C'est pour cela que le refus propose d'abord de RÉTABLIR le fichier, et ce
623
+ ré-alignement seulement en second — ne l'utilisez qu'en sachant que la modification était sans
624
+ effet (une reformulation, un commentaire).
625
+ - **`orm:migrate --dry-run` n'écrit rien du tout** — pas même la table d'historique, et il ne prend
626
+ pas le verrou. Un compte en lecture seule suffit donc à voir le plan, et la base reste
627
+ bit-à-bit celle d'avant : c'est ce qui rend l'essai vérifiable.
628
+ - **Une migration en attente qui se range AVANT la dernière appliquée est refusée** (deux branches
629
+ fusionnées). `--out-of-order` l'assume, et il faut l'assumer sciemment : l'ordre d'application ne
630
+ sera plus celui du journal.
631
+ - **En développement, une colonne obligatoire ajoutée à une entité ne se rattrape pas.** C'est le
632
+ cas le plus fréquent après le cas nullable, et la seule issue est `orm:reset` — ou une vraie
633
+ migration si la base porte des données auxquelles vous tenez.
634
+ - **Le rattrapage automatique n'existe qu'en mode `auto`.** En `migrate` et `none`, la comparaison
635
+ constate et ne répare jamais.
636
+ - **Un tag et un nom de source sont SENSIBLES À LA CASSE.** `--up-to 0003_Audit` et `--source App`
637
+ sont refusés, en nommant la bonne graphie. Ce n'est pas du zèle : sans point d'arrêt reconnu,
638
+ l'adoption déclarerait à niveau **tout** l'historique — et une base ne reçoit jamais une migration
639
+ qu'elle croit déjà avoir. Le refus sur `--source` évite l'autre moitié du piège : filtrer sur un
640
+ nom inconnu ne touche rien et rend pourtant « rien à réparer ».
641
+ - **Un `.sql` peut être écrit avec une marque d'ordre des octets** (les éditeurs Windows et
642
+ PowerShell la posent). Elle est retirée à la lecture, et ne compte ni dans le marqueur de format
643
+ ni dans l'empreinte : un dépôt relu sous Windows ne fait donc pas diverger les empreintes posées
644
+ par l'image Linux qui a migré la base d'équipe.
645
+ - **Une ligne qui commence par deux tirets À L'INTÉRIEUR d'une chaîne littérale reste de la
646
+ donnée.** C'est le cas d'un remplissage textuel multi-ligne écrit avec `orm:generate --custom` :
647
+ la retirer changerait silencieusement ce qui est inséré.
648
+ - **Le séparateur d'instructions écrit DANS un commentaire n'en est pas un.** `--> statement-breakpoint`
649
+ commence lui-même par deux tirets : rien ne le distingue d'un commentaire à l'œil du moteur. Une
650
+ ligne de commentaire qui le CITE — ce que fait le gabarit d'une migration libre — ne coupe donc
651
+ rien.
652
+
653
+ ## 🧪 Tests & couverture
654
+
655
+ | Ce qui est prouvé | Où |
656
+ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
657
+ | Applicateur : identité ensembliste, dérive, ordre, échec puis réparation, adoption, idempotence | `tests/integration/migrator-sqlite.test.ts` |
658
+ | Verrou entre process, absence de zombie, DDL non transactionnel | `migrator-postgres.e2e.test.ts`, `migrator-mysql.e2e.test.ts` |
659
+ | Les cinq verbes sur un **boot réel**, dans les trois modes | `migrate-cli.e2e.test.ts` (`NF_RUN_CLI_BOOT=1`) |
660
+ | Rattrapage additif, refus d'inventer, colonne en trop ignorée | `schema-reconcile.test.ts` |
661
+ | Rattrapage sur **serveurs réels** (types, catalogue, index) | `schema-reconcile-dialects.e2e.test.ts` |
662
+ | Verdict `divergent`, son absence quand un geste est déjà dû, et le DÉTAIL qu'il nomme | `migrate-divergence.test.ts` |
663
+ | Refus destructif : ce qui perd des données, ce qui n'en perd pas | `migrate-destructive.test.ts` |
664
+ | Parité entre le schéma migré et le schéma dérivé, sur les 3 dialectes | `migrations-parity-*.test.ts` |
665
+ | **Chaque réglage** sur son couple (refus sans lui, travail avec lui), 3 dialectes | `migrate-reglages.test.ts` |
666
+ | Lecture et empreinte d'un fichier : marque d'ordre des octets, CRLF, chaînes littérales | `tests/unit/migrationFichiers.test.ts` |
667
+ | Le nom d'une migration, et l'invariant « une suggestion est toujours acceptable » | `tests/unit/migrationName.test.ts` |
668
+ | Le cycle complet **dans une application générée** : génération, barrière de déploiement, `/readyz` 503, divergence provoquée | gabarit `tests/migrations.e2e.test.ts` de toute app à ORM |
669
+ | Le découpage en instructions : séparateur dans un commentaire, séparateur collé en fin de ligne | `tests/unit/migratorContracts.test.ts` |
670
+
671
+ Les bancs PostgreSQL et MySQL/MariaDB exigent leurs serveurs :
672
+
673
+ ```bash
674
+ docker compose -f docker/docker-compose.yml --profile postgres up -d postgres
675
+ docker compose -f docker/docker-compose.yml --profile mariadb up -d mariadb
676
+ NF_PG_URL=postgres://nodefony:nodefony-dev@127.0.0.1:5432/nodefony \
677
+ NF_MYSQL_URL=mysql://nodefony:nodefony-dev@127.0.0.1:3306/nodefony npm test
678
+ ```
679
+
680
+ > [!CAUTION]
681
+ > Sans ces variables, les bancs se **skippent** — et un test skippé compte comme vert. La suite du
682
+ > module affiche en fin de passe ce qu'elle n'a PAS exercé : lisez ce bloc avant de conclure.
683
+
684
+ ## 🔗 Pour aller plus loin
685
+
686
+ - ⬆️ **Retour au hub** : [@nodefony/drizzle](index.md)
687
+ - [Configuration du module](index.md#configuration) — la clé `ddl` et le bloc `migrations`
688
+ - [Dialectes](index.md#dialectes--une-base-par-déploiement-un-seul-code) — porter ses entités sur
689
+ PostgreSQL et MySQL
690
+ - [Les huit stores du framework](index.md#les-huit-stores-du-framework--la-persistance-clé-en-main) —
691
+ ce que le paquet livre déjà, migrations comprises