@nodefony/drizzle 10.0.0-alpha.6 → 10.0.0-alpha.8
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.
- package/README.md +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +4 -3
- package/dist/nodefony/command/orm-generate.js +70 -6
- package/dist/nodefony/service/DrizzleService.js +2 -2
- package/dist/nodefony/src/migrator/DrizzleMigrator.js +17 -1
- package/dist/nodefony/src/migrator/destructive.js +78 -19
- package/dist/nodefony/src/migrator/explain.js +42 -1
- package/dist/nodefony/src/migrator/kit.js +5 -0
- package/dist/nodefony/src/migrator/resolve.js +74 -4
- package/dist/nodefony/src/orm-core/DrizzleOrm.js +121 -21
- package/dist/nodefony/src/orm-core/ddlPlan.js +95 -0
- package/dist/nodefony/src/orm-core/index.js +2 -1
- package/dist/nodefony/src/orm-core/seedRow.js +65 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/nodefony/command/orm-generate.d.ts +12 -0
- package/dist/types/nodefony/src/migrator/destructive.d.ts +36 -0
- package/dist/types/nodefony/src/migrator/explain.d.ts +19 -0
- package/dist/types/nodefony/src/migrator/resolve.d.ts +54 -1
- package/dist/types/nodefony/src/orm-core/ddlPlan.d.ts +96 -0
- package/dist/types/nodefony/src/orm-core/index.d.ts +1 -0
- package/dist/types/nodefony/src/orm-core/seedRow.d.ts +23 -0
- package/docs/migrations.md +30 -1
- package/package.json +15 -15
|
@@ -5,6 +5,7 @@ import { describeTargetSafely } from "../safeTarget.js";
|
|
|
5
5
|
import { failureFrom } from "../migrator/status.js";
|
|
6
6
|
import { DrizzleRepository } from "./DrizzleRepository.js";
|
|
7
7
|
import { DrizzleTransaction } from "./DrizzleTransaction.js";
|
|
8
|
+
import { explainOmittedForeignKeys, planTableCreation } from "./ddlPlan.js";
|
|
8
9
|
import { createRequire } from "node:module";
|
|
9
10
|
import { CONNECT_TIMEOUT_MS, Orm, entityRegistry, parseConnectionTarget, withConnectDeadline } from "@nodefony/orm-core";
|
|
10
11
|
import fs from "node:fs";
|
|
@@ -15,6 +16,32 @@ import { PgDialect, PgTable, getTableConfig as getTableConfig$1 } from "drizzle-
|
|
|
15
16
|
import { MySqlDialect, MySqlTable, getTableConfig as getTableConfig$2 } from "drizzle-orm/mysql-core";
|
|
16
17
|
//#region nodefony/src/orm-core/DrizzleOrm.ts
|
|
17
18
|
/**
|
|
19
|
+
* Traduit les clés étrangères d'une table Drizzle en noms SQL émettables.
|
|
20
|
+
*
|
|
21
|
+
* @param foreignKeys - contraintes lues sur la table (`getTableConfig`).
|
|
22
|
+
* @param nameOf - comment lire le nom SQL d'une table du dialecte courant.
|
|
23
|
+
* @param skip - tables visées dont la contrainte doit être ABANDONNÉE (cycle ou
|
|
24
|
+
* cible absente de cette base) ; l'appelant les a déjà journalisées.
|
|
25
|
+
* @returns les contraintes à écrire dans le `CREATE TABLE`.
|
|
26
|
+
*/
|
|
27
|
+
function resolveForeignKeys(foreignKeys, nameOf, skip) {
|
|
28
|
+
const resolved = [];
|
|
29
|
+
for (const fk of foreignKeys) {
|
|
30
|
+
const reference = fk.reference();
|
|
31
|
+
const foreignTable = nameOf(reference.foreignTable);
|
|
32
|
+
if (skip?.has(foreignTable)) continue;
|
|
33
|
+
resolved.push({
|
|
34
|
+
name: fk.getName(),
|
|
35
|
+
columns: reference.columns.map((column) => column.name),
|
|
36
|
+
foreignTable,
|
|
37
|
+
foreignColumns: reference.foreignColumns.map((column) => column.name),
|
|
38
|
+
onDelete: fk.onDelete,
|
|
39
|
+
onUpdate: fk.onUpdate
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
18
45
|
* Rend le prédicat d'une contrainte `CHECK` en SQL littéral, avec la grammaire
|
|
19
46
|
* du dialecte demandé.
|
|
20
47
|
*
|
|
@@ -245,7 +272,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
|
|
|
245
272
|
* `"…"` (SQL standard, SQLite/PG) vs backtick MySQL (qui ne lit `"…"` qu'en
|
|
246
273
|
* mode ANSI_QUOTES, jamais garanti). La prod reste pilotée par drizzle-kit.
|
|
247
274
|
*/
|
|
248
|
-
#buildCreateTable(name, columns, quote = "\"", checks = [], dialect = "sqlite") {
|
|
275
|
+
#buildCreateTable(name, columns, quote = "\"", checks = [], dialect = "sqlite", foreignKeys = []) {
|
|
249
276
|
const defs = columns.map((col) => {
|
|
250
277
|
const parts = [`${quote}${col.name}${quote}`, col.getSQLType()];
|
|
251
278
|
if (col.primary) parts.push("PRIMARY KEY");
|
|
@@ -258,6 +285,12 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
|
|
|
258
285
|
if (predicate === null) continue;
|
|
259
286
|
defs.push(`CONSTRAINT ${quote}${check.name}${quote} CHECK (${predicate})`);
|
|
260
287
|
}
|
|
288
|
+
for (const fk of foreignKeys) {
|
|
289
|
+
const source = fk.columns.map((c) => `${quote}${c}${quote}`).join(", ");
|
|
290
|
+
const target = fk.foreignColumns.map((c) => `${quote}${c}${quote}`).join(", ");
|
|
291
|
+
const actions = [fk.onDelete ? ` ON DELETE ${fk.onDelete.toUpperCase()}` : "", fk.onUpdate ? ` ON UPDATE ${fk.onUpdate.toUpperCase()}` : ""].join("");
|
|
292
|
+
defs.push(`CONSTRAINT ${quote}${fk.name}${quote} FOREIGN KEY (${source}) REFERENCES ${quote}${fk.foreignTable}${quote} (${target})${actions}`);
|
|
293
|
+
}
|
|
261
294
|
return `CREATE TABLE IF NOT EXISTS ${quote}${name}${quote} (${defs.join(", ")})`;
|
|
262
295
|
}
|
|
263
296
|
/**
|
|
@@ -273,11 +306,11 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
|
|
|
273
306
|
* pas. Émis à part, il arrive AUSSI sur une base de développement déjà créée —
|
|
274
307
|
* ce qu'aucune clause de table ne permettrait.
|
|
275
308
|
*
|
|
276
|
-
* Les clés étrangères, elles, ne
|
|
277
|
-
* DANS le `CREATE TABLE
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
309
|
+
* Les clés étrangères, elles, ne peuvent PAS être émises à part : elles se
|
|
310
|
+
* déclarent DANS le `CREATE TABLE` (cf `#buildCreateTable`), donc elles
|
|
311
|
+
* n'atteignent jamais une base déjà créée — c'est le prix assumé, le même que
|
|
312
|
+
* pour les `CHECK`. Et elles imposent de créer les tables dans l'ordre de
|
|
313
|
+
* leurs dépendances : cf `#planCreation`, qui porte aussi le cas du cycle.
|
|
281
314
|
*
|
|
282
315
|
* @param table - nom de la table portant les index.
|
|
283
316
|
* @param indexes - index déclarés (`getTableConfig(...).indexes`).
|
|
@@ -295,10 +328,43 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
|
|
|
295
328
|
}
|
|
296
329
|
return statements;
|
|
297
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Ordonne les tables pour que chaque cible naisse avant qui la désigne, et dit
|
|
333
|
+
* ce qu'il a fallu abandonner.
|
|
334
|
+
*
|
|
335
|
+
* Le tri lui-même vit à part ({@link planTableCreation}, module pur) ; cette
|
|
336
|
+
* méthode ne fait que lire les tables du dialecte courant et JOURNALISER.
|
|
337
|
+
* L'avertissement compte autant que le tri : une contrainte d'intégrité qui
|
|
338
|
+
* n'est pas posée est une garantie qu'on croit avoir.
|
|
339
|
+
*
|
|
340
|
+
* @param entities - les entités de ce connecteur.
|
|
341
|
+
* @param describe - comment lire le nom d'une table et ses cibles.
|
|
342
|
+
* @returns les entités dans l'ordre de création, et les cibles à ignorer par table.
|
|
343
|
+
*/
|
|
344
|
+
#planCreation(entities, describe) {
|
|
345
|
+
const described = entities.map((entity) => ({
|
|
346
|
+
entity,
|
|
347
|
+
...describe(entity)
|
|
348
|
+
}));
|
|
349
|
+
const plan = planTableCreation(described);
|
|
350
|
+
const skip = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const entry of plan.omitted) {
|
|
352
|
+
const targets = skip.get(entry.table) ?? /* @__PURE__ */ new Set();
|
|
353
|
+
targets.add(entry.target);
|
|
354
|
+
skip.set(entry.table, targets);
|
|
355
|
+
}
|
|
356
|
+
const message = explainOmittedForeignKeys(plan.omitted);
|
|
357
|
+
if (message) this.log(message, "WARNING");
|
|
358
|
+
const rank = new Map(plan.order.map((name, index) => [name, index]));
|
|
359
|
+
return {
|
|
360
|
+
ordered: [...described].sort((a, b) => (rank.get(a.name) ?? 0) - (rank.get(b.name) ?? 0)).map((entry) => entry.entity),
|
|
361
|
+
skip
|
|
362
|
+
};
|
|
363
|
+
}
|
|
298
364
|
/** Dérive le `CREATE TABLE` SQLite depuis la table Drizzle (dev/test). */
|
|
299
|
-
#createTableSQL(table) {
|
|
300
|
-
const { name, columns, checks } = getTableConfig(table);
|
|
301
|
-
return this.#buildCreateTable(name, columns, "\"", checks, "sqlite");
|
|
365
|
+
#createTableSQL(table, skip) {
|
|
366
|
+
const { name, columns, checks, foreignKeys } = getTableConfig(table);
|
|
367
|
+
return this.#buildCreateTable(name, columns, "\"", checks, "sqlite", resolveForeignKeys(foreignKeys, (target) => getTableConfig(target).name, skip));
|
|
302
368
|
}
|
|
303
369
|
/** Dérive les `CREATE INDEX` SQLite depuis la table Drizzle (dev/test). */
|
|
304
370
|
#createIndexesSQL(table) {
|
|
@@ -306,9 +372,9 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
|
|
|
306
372
|
return this.#buildCreateIndexes(name, indexes);
|
|
307
373
|
}
|
|
308
374
|
/** Dérive le `CREATE TABLE` Postgres depuis la table Drizzle (dev/test). */
|
|
309
|
-
#createTablePgSQL(table) {
|
|
310
|
-
const { name, columns, checks } = getTableConfig$1(table);
|
|
311
|
-
return this.#buildCreateTable(name, columns, "\"", checks, "postgres");
|
|
375
|
+
#createTablePgSQL(table, skip) {
|
|
376
|
+
const { name, columns, checks, foreignKeys } = getTableConfig$1(table);
|
|
377
|
+
return this.#buildCreateTable(name, columns, "\"", checks, "postgres", resolveForeignKeys(foreignKeys, (target) => getTableConfig$1(target).name, skip));
|
|
312
378
|
}
|
|
313
379
|
/** Dérive les `CREATE INDEX` Postgres depuis la table Drizzle (dev/test). */
|
|
314
380
|
#createIndexesPgSQL(table) {
|
|
@@ -316,9 +382,9 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
|
|
|
316
382
|
return this.#buildCreateIndexes(name, indexes);
|
|
317
383
|
}
|
|
318
384
|
/** Dérive le `CREATE TABLE` MySQL depuis la table Drizzle (dev/test). */
|
|
319
|
-
#createTableMysqlSQL(table) {
|
|
320
|
-
const { name, columns, checks } = getTableConfig$2(table);
|
|
321
|
-
return this.#buildCreateTable(name, columns, "`", checks, "mysql");
|
|
385
|
+
#createTableMysqlSQL(table, skip) {
|
|
386
|
+
const { name, columns, checks, foreignKeys } = getTableConfig$2(table);
|
|
387
|
+
return this.#buildCreateTable(name, columns, "`", checks, "mysql", resolveForeignKeys(foreignKeys, (target) => getTableConfig$2(target).name, skip));
|
|
322
388
|
}
|
|
323
389
|
/**
|
|
324
390
|
* Dérive les `CREATE INDEX` MySQL depuis la table Drizzle (dev/test).
|
|
@@ -411,6 +477,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
|
|
|
411
477
|
client.pragma("journal_mode = WAL");
|
|
412
478
|
client.pragma("synchronous = NORMAL");
|
|
413
479
|
}
|
|
480
|
+
client.pragma("foreign_keys = ON");
|
|
414
481
|
this.#client = client;
|
|
415
482
|
const db = sqliteDrizzle(client);
|
|
416
483
|
this.#db = db;
|
|
@@ -440,8 +507,19 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
|
|
|
440
507
|
const table = entity.schema;
|
|
441
508
|
this.#tables[entity.name] = table;
|
|
442
509
|
entity.model = table;
|
|
443
|
-
|
|
444
|
-
|
|
510
|
+
}
|
|
511
|
+
if (!this.#deriveSchema) return;
|
|
512
|
+
const plan = this.#planCreation(entities, (entity) => {
|
|
513
|
+
const config = getTableConfig(entity.schema);
|
|
514
|
+
return {
|
|
515
|
+
name: config.name,
|
|
516
|
+
references: config.foreignKeys.map((fk) => getTableConfig(fk.reference().foreignTable).name)
|
|
517
|
+
};
|
|
518
|
+
});
|
|
519
|
+
for (const entity of plan.ordered) {
|
|
520
|
+
const table = entity.schema;
|
|
521
|
+
const name = getTableConfig(table).name;
|
|
522
|
+
client.exec(this.#createTableSQL(table, plan.skip.get(name)));
|
|
445
523
|
}
|
|
446
524
|
}
|
|
447
525
|
/**
|
|
@@ -671,8 +749,19 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
|
|
|
671
749
|
const table = entity.schema;
|
|
672
750
|
this.#tables[entity.name] = table;
|
|
673
751
|
entity.model = table;
|
|
674
|
-
|
|
675
|
-
|
|
752
|
+
}
|
|
753
|
+
if (!this.#deriveSchema) return;
|
|
754
|
+
const plan = this.#planCreation(entities, (entity) => {
|
|
755
|
+
const config = getTableConfig$1(entity.schema);
|
|
756
|
+
return {
|
|
757
|
+
name: config.name,
|
|
758
|
+
references: config.foreignKeys.map((fk) => getTableConfig$1(fk.reference().foreignTable).name)
|
|
759
|
+
};
|
|
760
|
+
});
|
|
761
|
+
for (const entity of plan.ordered) {
|
|
762
|
+
const table = entity.schema;
|
|
763
|
+
const name = getTableConfig$1(table).name;
|
|
764
|
+
await pool.query(this.#createTablePgSQL(table, plan.skip.get(name)));
|
|
676
765
|
}
|
|
677
766
|
}
|
|
678
767
|
/**
|
|
@@ -849,8 +938,19 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
|
|
|
849
938
|
const table = entity.schema;
|
|
850
939
|
this.#tables[entity.name] = table;
|
|
851
940
|
entity.model = table;
|
|
852
|
-
|
|
853
|
-
|
|
941
|
+
}
|
|
942
|
+
if (!this.#deriveSchema) return;
|
|
943
|
+
const plan = this.#planCreation(entities, (entity) => {
|
|
944
|
+
const config = getTableConfig$2(entity.schema);
|
|
945
|
+
return {
|
|
946
|
+
name: config.name,
|
|
947
|
+
references: config.foreignKeys.map((fk) => getTableConfig$2(fk.reference().foreignTable).name)
|
|
948
|
+
};
|
|
949
|
+
});
|
|
950
|
+
for (const entity of plan.ordered) {
|
|
951
|
+
const table = entity.schema;
|
|
952
|
+
const name = getTableConfig$2(table).name;
|
|
953
|
+
await pool.query(this.#createTableMysqlSQL(table, plan.skip.get(name)));
|
|
854
954
|
}
|
|
855
955
|
}
|
|
856
956
|
async disconnect() {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region nodefony/src/orm-core/ddlPlan.ts
|
|
2
|
+
/**
|
|
3
|
+
* Trie les tables pour que chaque cible naisse avant qui la désigne.
|
|
4
|
+
*
|
|
5
|
+
* Tri topologique (Kahn) sur le graphe des dépendances, avec deux écarts
|
|
6
|
+
* assumés au manuel :
|
|
7
|
+
*
|
|
8
|
+
* - **l'auto-référence ne compte pas** (`parent:ref:Category` dans `Category`) :
|
|
9
|
+
* les trois moteurs acceptent une contrainte qui vise la table en cours de
|
|
10
|
+
* création, dans le même `CREATE TABLE` ;
|
|
11
|
+
* - **un cycle ne fait pas échouer le démarrage**. Il n'y a aucun ordre correct,
|
|
12
|
+
* mais refuser de démarrer pour ça serait hors de proportion : on crée les
|
|
13
|
+
* tables du cycle dans un ordre stable, et on OMET les contraintes qui
|
|
14
|
+
* regardent en arrière. Elles sont rendues à l'appelant pour qu'il les dise —
|
|
15
|
+
* une garantie qui disparaît sans un mot est pire que son absence.
|
|
16
|
+
*
|
|
17
|
+
* À ordre de sortie stable : les tables prêtes sont prises par ordre
|
|
18
|
+
* alphabétique, jamais dans l'ordre d'arrivée. Deux démarrages de la même
|
|
19
|
+
* application produisent donc le même DDL, ce qui rend un journal comparable.
|
|
20
|
+
*
|
|
21
|
+
* @param tables - les tables du connecteur et leurs cibles.
|
|
22
|
+
* @returns l'ordre de création et les contraintes abandonnées.
|
|
23
|
+
*/
|
|
24
|
+
function planTableCreation(tables) {
|
|
25
|
+
const known = new Set(tables.map((table) => table.name));
|
|
26
|
+
const omitted = [];
|
|
27
|
+
/** Dépendances RÉELLES : cible connue, et différente de la table elle-même. */
|
|
28
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
29
|
+
for (const table of tables) {
|
|
30
|
+
const deps = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const target of table.references) {
|
|
32
|
+
if (target === table.name) continue;
|
|
33
|
+
if (!known.has(target)) {
|
|
34
|
+
omitted.push({
|
|
35
|
+
table: table.name,
|
|
36
|
+
target,
|
|
37
|
+
reason: "absent"
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
deps.add(target);
|
|
42
|
+
}
|
|
43
|
+
dependencies.set(table.name, deps);
|
|
44
|
+
}
|
|
45
|
+
const order = [];
|
|
46
|
+
const placed = /* @__PURE__ */ new Set();
|
|
47
|
+
const remaining = new Set(known);
|
|
48
|
+
for (;;) {
|
|
49
|
+
const ready = [...remaining].filter((name) => {
|
|
50
|
+
const deps = dependencies.get(name);
|
|
51
|
+
return !deps || [...deps].every((dep) => placed.has(dep));
|
|
52
|
+
}).sort();
|
|
53
|
+
if (ready.length === 0) break;
|
|
54
|
+
for (const name of ready) {
|
|
55
|
+
order.push(name);
|
|
56
|
+
placed.add(name);
|
|
57
|
+
remaining.delete(name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const name of [...remaining].sort()) {
|
|
61
|
+
order.push(name);
|
|
62
|
+
placed.add(name);
|
|
63
|
+
}
|
|
64
|
+
const rank = new Map(order.map((name, index) => [name, index]));
|
|
65
|
+
for (const name of order) for (const target of dependencies.get(name) ?? []) if ((rank.get(target) ?? 0) > (rank.get(name) ?? 0)) omitted.push({
|
|
66
|
+
table: name,
|
|
67
|
+
target,
|
|
68
|
+
reason: "cycle"
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
order,
|
|
72
|
+
omitted
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Met en mots les contraintes abandonnées — le message que l'appelant journalise.
|
|
77
|
+
*
|
|
78
|
+
* Écrit pour être lu par celui qui découvre le problème dans ses journaux : il
|
|
79
|
+
* nomme les tables, la cause, et ce qu'il reste à faire. Un avertissement qui ne
|
|
80
|
+
* dit pas quoi faire se relit deux fois puis s'ignore.
|
|
81
|
+
*
|
|
82
|
+
* @param omitted - les contraintes non émises.
|
|
83
|
+
* @returns le texte à journaliser, ou `null` s'il n'y a rien à dire.
|
|
84
|
+
*/
|
|
85
|
+
function explainOmittedForeignKeys(omitted) {
|
|
86
|
+
if (omitted.length === 0) return null;
|
|
87
|
+
const cycles = omitted.filter((entry) => entry.reason === "cycle");
|
|
88
|
+
const absent = omitted.filter((entry) => entry.reason === "absent");
|
|
89
|
+
const parts = [];
|
|
90
|
+
if (cycles.length > 0) parts.push(`cycle entre entités — contrainte(s) NON posée(s) : ${cycles.map((c) => `${c.table} → ${c.target}`).join(", ")}. Aucun ordre de création ne satisfait les deux sens ; une migration (\`orm:generate\`) les ajoute après coup par \`ALTER TABLE\`.`);
|
|
91
|
+
if (absent.length > 0) parts.push(`cible hors de cette base — contrainte(s) NON posée(s) : ${absent.map((a) => `${a.table} → ${a.target}`).join(", ")}. L'entité visée est déclarée sur un autre connecteur : l'intégrité référentielle ne traverse pas deux bases.`);
|
|
92
|
+
return parts.join(" ");
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { explainOmittedForeignKeys, planTableCreation };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DrizzleRepository } from "./DrizzleRepository.js";
|
|
2
2
|
import { DrizzleTransaction } from "./DrizzleTransaction.js";
|
|
3
3
|
import { DrizzleOrm } from "./DrizzleOrm.js";
|
|
4
|
-
|
|
4
|
+
import { seedEntityRow } from "./seedRow.js";
|
|
5
|
+
export { DrizzleOrm, DrizzleRepository, DrizzleTransaction, seedEntityRow };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getTableColumns } from "drizzle-orm";
|
|
2
|
+
//#region nodefony/src/orm-core/seedRow.ts
|
|
3
|
+
/**
|
|
4
|
+
* Compteur de semis — il rend chaque ligne semée DISTINCTE.
|
|
5
|
+
*
|
|
6
|
+
* Une entité cible porte souvent une colonne unique (un courriel, une
|
|
7
|
+
* référence). Semer deux fois la même valeur violerait sa contrainte, et le test
|
|
8
|
+
* échouerait sur son propre décor plutôt que sur ce qu'il éprouve.
|
|
9
|
+
*/
|
|
10
|
+
let seedCounter = 0;
|
|
11
|
+
/**
|
|
12
|
+
* Valeur plausible pour une colonne, dérivée de ce que Drizzle en dit.
|
|
13
|
+
*
|
|
14
|
+
* On lit `dataType`, la classification **générique** de Drizzle (commune aux
|
|
15
|
+
* trois dialectes), et non `columnType`, qui nomme l'implémentation (`SQLiteText`,
|
|
16
|
+
* `PgVarchar`) et obligerait à connaître chaque moteur.
|
|
17
|
+
*/
|
|
18
|
+
function sampleForColumn(column, n) {
|
|
19
|
+
if (column.enumValues?.length) return column.enumValues[0];
|
|
20
|
+
switch (column.dataType) {
|
|
21
|
+
case "number": return n;
|
|
22
|
+
case "bigint": return BigInt(n);
|
|
23
|
+
case "boolean": return true;
|
|
24
|
+
case "date": return /* @__PURE__ */ new Date();
|
|
25
|
+
case "json": return {};
|
|
26
|
+
case "buffer": return Buffer.from(`seed-${n}`);
|
|
27
|
+
default: return `seed-${n}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Insère une ligne minimale dans la table d'une entité, et rend son identifiant.
|
|
32
|
+
*
|
|
33
|
+
* Destiné aux **tests** : dès qu'une entité porte une relation (`author:ref:Author`),
|
|
34
|
+
* sa colonne est une vraie clé étrangère, et la base REFUSE un identifiant
|
|
35
|
+
* inventé. La ligne parente doit donc exister avant toute insertion — et c'est
|
|
36
|
+
* la seule façon d'éprouver la contrainte au lieu de la contourner (débrancher
|
|
37
|
+
* `foreign_keys` rendrait le test vert sans rien prouver).
|
|
38
|
+
*
|
|
39
|
+
* Ce qui est rempli : les colonnes **obligatoires sans valeur par défaut**, avec
|
|
40
|
+
* une valeur du type que Drizzle déclare. Le reste est laissé au schéma — une
|
|
41
|
+
* clé primaire auto-générée, un horodatage, un défaut applicatif se posent seuls.
|
|
42
|
+
*
|
|
43
|
+
* @param orm - la connexion sur laquelle semer.
|
|
44
|
+
* @param entity - l'entité cible ; son `schema` est la table Drizzle.
|
|
45
|
+
* @returns l'identifiant de la ligne créée, à poser dans la colonne qui la désigne.
|
|
46
|
+
* @throws Si la cible porte elle-même une clé étrangère obligatoire — le semis à
|
|
47
|
+
* un seul étage ne sait pas la satisfaire, et le message le dit plutôt que de
|
|
48
|
+
* laisser remonter un « FOREIGN KEY constraint failed » sans contexte.
|
|
49
|
+
*/
|
|
50
|
+
async function seedEntityRow(orm, entity) {
|
|
51
|
+
const n = ++seedCounter;
|
|
52
|
+
const columns = getTableColumns(entity.schema);
|
|
53
|
+
const row = {};
|
|
54
|
+
for (const [name, column] of Object.entries(columns)) {
|
|
55
|
+
if (!column.notNull || column.hasDefault || column.primary) continue;
|
|
56
|
+
row[name] = sampleForColumn(column, n);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return (await orm.getRepository(entity.name).create(row)).id;
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
throw new Error(`seedEntityRow : impossible de semer une ligne de « ${entity.name} ». Si cette entité porte elle-même une relation obligatoire, sème d'abord sa cible. Cause : ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { seedEntityRow };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export { DrizzleMigrator, DEFAULT_LOCK_TIMEOUT_MS, openMigrationDriver, framewor
|
|
|
48
48
|
export { MIGRATION_FORMAT_VERSION, EXIT, buildReport, verdictOf, exitCodeOf, meaningOf, renderStatus, renderRefusal, styleFor, action, type IMigrationReport, type IMigrationSourceReport, type IReportContext, type MigrationVerdictName, type IStyle, } from "./nodefony/src/migrator/explain.js";
|
|
49
49
|
export { MIGRATE_URL_ENV, readMigrationEnv, resolveDdlMode, resolveCheckMode, resetAllowed, resolveConnector, resolveDivergenceMode, knownConnectors, appMigrationsDir, buildMigrator, type IMigrationEnv, type IConnectorResolution, } from "./nodefony/src/migrator/resolve.js";
|
|
50
50
|
export { DDL_MODES, MIGRATION_CHECK_MODES, DIVERGENCE_MODES, type DdlMode, type MigrationCheckMode, type DivergenceMode, } from "./nodefony/config/config.js";
|
|
51
|
-
export { DrizzleOrm, DrizzleRepository, DrizzleTransaction, } from "./nodefony/src/orm-core/index.js";
|
|
51
|
+
export { DrizzleOrm, DrizzleRepository, DrizzleTransaction, seedEntityRow, } from "./nodefony/src/orm-core/index.js";
|
|
52
52
|
export type { DrizzleOrmOptions, DrizzleDb, DrizzleResolvedRelation, } from "./nodefony/src/orm-core/index.js";
|
|
53
53
|
export { userTable, createUserTable, createUserEntity, registerUserEntity, } from "./nodefony/entity/userTable.js";
|
|
54
54
|
export type { UserRow } from "./nodefony/entity/userTable.js";
|
|
@@ -5,6 +5,7 @@ interface IGenerateOptions extends IMigrateSharedOptions {
|
|
|
5
5
|
name?: string;
|
|
6
6
|
custom?: boolean;
|
|
7
7
|
allowDestructive?: boolean;
|
|
8
|
+
apply?: boolean;
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
11
|
* `nodefony orm:generate` — écrit la migration qui fera passer la base au
|
|
@@ -39,6 +40,17 @@ interface IGenerateOptions extends IMigrateSharedOptions {
|
|
|
39
40
|
* nodefony orm:migrate # ou : le travail de déploiement
|
|
40
41
|
* ```
|
|
41
42
|
*
|
|
43
|
+
* @example En développement — écrire ET appliquer en un seul geste
|
|
44
|
+
* ```bash
|
|
45
|
+
* nodefony orm:generate --name ajout_du_titre --apply
|
|
46
|
+
* ```
|
|
47
|
+
* `--apply` n'est rien d'autre qu'un `orm:migrate` enchaîné : la commande
|
|
48
|
+
* DÉLÈGUE, elle ne rejoue pas l'application pour son compte. Toutes les gardes
|
|
49
|
+
* du verbe d'application valent donc à l'identique — refus du destructif sans
|
|
50
|
+
* `--allow-destructive`, verrou, historique. Refusé hors développement : un
|
|
51
|
+
* déploiement n'écrit pas ses migrations au moment de les appliquer, il
|
|
52
|
+
* applique celles que la revue de code a validées.
|
|
53
|
+
*
|
|
42
54
|
* @example Ce que le modèle déclaratif ne peut pas déduire
|
|
43
55
|
* ```bash
|
|
44
56
|
* nodefony orm:generate --custom --name vue_des_ventes
|
|
@@ -62,6 +62,42 @@ export interface IDestructiveFinding {
|
|
|
62
62
|
/** L'instruction elle-même, bornée pour rester lisible. */
|
|
63
63
|
statement: string;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Les tables qu'une migration RECONSTRUIT, par la ronde que SQLite impose.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 **La convention de nommage n'est PAS la règle.** La version précédente
|
|
69
|
+
* cherchait le préfixe `__new_` que produit l'outil de génération : elle ne
|
|
70
|
+
* reconnaissait donc une reconstruction que si c'est LUI qui l'avait écrite.
|
|
71
|
+
* Une migration reprise à la main — cas courant, et mesuré sur un agent réel
|
|
72
|
+
* qui avait nommé sa table d'étape `articles_new` — retombait sur
|
|
73
|
+
* « supprime une table et TOUTES ses lignes », sur un SQL qui ne perd rien.
|
|
74
|
+
*
|
|
75
|
+
* Ce qui fait une reconstruction, c'est la RONDE, quels que soient les noms :
|
|
76
|
+
* une table d'étape est créée, les lignes de la table visée y sont recopiées,
|
|
77
|
+
* la visée est supprimée, et l'étape prend sa place. Les quatre doivent être
|
|
78
|
+
* présentes dans le même fichier, et se RÉPONDRE — c'est le renommage final
|
|
79
|
+
* qui dit quelle table d'étape appartient à quelle table visée.
|
|
80
|
+
*
|
|
81
|
+
* ⚠️ Ce n'est pas une garantie que rien n'est perdu : si une colonne n'apparaît
|
|
82
|
+
* pas dans le `INSERT … SELECT`, ses données disparaissent bel et bien. La
|
|
83
|
+
* reconstruction reste donc SIGNALÉE, avec le geste — lire le SQL —, et
|
|
84
|
+
* seulement déclassée en avertissement.
|
|
85
|
+
*
|
|
86
|
+
* @param sql - les instructions de la migration, ou le fichier entier.
|
|
87
|
+
* @returns les tables reconstruites, en minuscules ; vide s'il n'y en a aucune.
|
|
88
|
+
*/
|
|
89
|
+
export declare function rebuiltTables(sql: readonly string[] | string): ReadonlySet<string>;
|
|
90
|
+
/**
|
|
91
|
+
* Les tables qu'un SQL SUPPRIME, dans l'ordre où il les nomme.
|
|
92
|
+
*
|
|
93
|
+
* Exportée pour que l'audit de génération et celui d'application lisent la
|
|
94
|
+
* MÊME chose : deux extractions écrites séparément divergeraient, et l'une des
|
|
95
|
+
* deux surfaces se mettrait à refuser ce que l'autre laisse passer.
|
|
96
|
+
*
|
|
97
|
+
* @param sql - les instructions de la migration, ou le fichier entier.
|
|
98
|
+
* @returns les noms supprimés, en minuscules.
|
|
99
|
+
*/
|
|
100
|
+
export declare function droppedTables(sql: readonly string[] | string): string[];
|
|
65
101
|
/**
|
|
66
102
|
* Cherche, dans les migrations en attente, ce qui détruit ou casse.
|
|
67
103
|
*
|
|
@@ -422,3 +422,22 @@ export declare function refusalInMode(code: IMigrationVerdict["code"], ddl: stri
|
|
|
422
422
|
meaning: string;
|
|
423
423
|
actions: IMigrationAction[];
|
|
424
424
|
} | null;
|
|
425
|
+
/**
|
|
426
|
+
* L'échec vient-il de ce que la base est DÉJÀ dans l'état que la migration vise ?
|
|
427
|
+
*
|
|
428
|
+
* Le cas est fréquent et sa sortie n'était nommée nulle part : le message de
|
|
429
|
+
* reprise proposait de corriger le fichier ou de travailler sur une base
|
|
430
|
+
* d'essai, deux voies qui ne mènent à rien quand le fichier est juste et la
|
|
431
|
+
* base déjà conforme. Un agent placé devant ce refus tourne alors en rond
|
|
432
|
+
* — `repair` lève le marqueur, `migrate` rejoue, le moteur redit la même
|
|
433
|
+
* chose — puis finit par recréer la base, ce qui emporte les données.
|
|
434
|
+
*
|
|
435
|
+
* Constaté sur un agent tiers : un `DROP INDEX messages_content_unique` sur une
|
|
436
|
+
* base dont l'unicité avait été posée INLINE par le DDL de démarrage, donc sans
|
|
437
|
+
* index nommé. Trente-cinq minutes, deux bases détruites, la boucle brisée à la
|
|
438
|
+
* main.
|
|
439
|
+
*
|
|
440
|
+
* @param message - le message d'erreur rendu par le moteur.
|
|
441
|
+
* @returns `true` si l'échec dit « c'est déjà ainsi ».
|
|
442
|
+
*/
|
|
443
|
+
export declare function alreadyInTargetState(message: string): boolean;
|
|
@@ -50,6 +50,23 @@ export declare function readMigrationEnv(kernel: Kernel | null): IMigrationEnv;
|
|
|
50
50
|
* @returns `true` si `orm:reset` est recevable.
|
|
51
51
|
*/
|
|
52
52
|
export declare function resetAllowed(env: IMigrationEnv): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* `orm:generate --apply` est-il recevable dans cet environnement ?
|
|
55
|
+
*
|
|
56
|
+
* Écrire une migration et l'appliquer dans le même souffle suppose qu'on relit
|
|
57
|
+
* le SQL produit entre les deux — ce que personne ne fait sur un serveur.
|
|
58
|
+
* Ailleurs qu'en développement, la migration a été écrite, relue et versionnée
|
|
59
|
+
* en amont : `orm:migrate` l'applique, et lui seul. Aucun drapeau ne lève ce
|
|
60
|
+
* refus, pour la même raison que {@link resetAllowed} n'en a pas.
|
|
61
|
+
*
|
|
62
|
+
* Même forme que `resetAllowed`, et c'est voulu : les deux disent « geste de
|
|
63
|
+
* développement », et deux rédactions de la même condition finiraient par
|
|
64
|
+
* diverger sans que rien ne le signale.
|
|
65
|
+
*
|
|
66
|
+
* @param env - environnement constaté.
|
|
67
|
+
* @returns `true` si l'enchaînement est recevable.
|
|
68
|
+
*/
|
|
69
|
+
export declare function generateApplyAllowed(env: IMigrationEnv): boolean;
|
|
53
70
|
/**
|
|
54
71
|
* Le mode de schéma qui s'applique à un connecteur.
|
|
55
72
|
*
|
|
@@ -62,11 +79,47 @@ export declare function resetAllowed(env: IMigrationEnv): boolean;
|
|
|
62
79
|
* - tout le reste → `none` : personne ne touche au schéma au démarrage, un
|
|
63
80
|
* travail externe applique les migrations avant que le trafic n'arrive.
|
|
64
81
|
*
|
|
82
|
+
* 🔴 **Une application qui VERSIONNE des migrations bascule en `migrate`, même
|
|
83
|
+
* en développement.** Deux fabricants du même schéma ne s'accordent pas, et
|
|
84
|
+
* rien ne le signale tant qu'on ne les fait pas se rencontrer : le DDL dérivé
|
|
85
|
+
* pose une contrainte d'unicité INLINE et anonyme (`content text NOT NULL
|
|
86
|
+
* UNIQUE`), là où le générateur de migrations raisonne sur un index NOMMÉ
|
|
87
|
+
* (`messages_content_unique`). La migration suivante émet alors un
|
|
88
|
+
* `DROP INDEX` d'un index qui n'a jamais existé, échoue sur `no such index`,
|
|
89
|
+
* inscrit son marqueur — et la réparation ne peut rien y faire, puisque le
|
|
90
|
+
* fichier est inapplicable. Vécu sur un agent tiers : trente-cinq minutes de
|
|
91
|
+
* `repair`/`baseline`/`reset`, deux bases détruites, la boucle brisée à la main.
|
|
92
|
+
* La condition est délibérément « l'application EN A », pas « elle en a en
|
|
93
|
+
* attente » : dès la première migration écrite, le schéma leur appartient.
|
|
94
|
+
* Elle est bornée au développement RÉEL, jamais à `NODE_ENV=test` : une suite
|
|
95
|
+
* lance un exemplaire par worker, et appliquer des migrations depuis plusieurs
|
|
96
|
+
* processus au démarrage est précisément ce que la règle ci-dessus refuse.
|
|
97
|
+
* Un `ddl` écrit dans la configuration gagne toujours — c'est un choix, pas une
|
|
98
|
+
* déduction.
|
|
99
|
+
*
|
|
65
100
|
* @param explicit - valeur écrite dans la configuration du connecteur, si elle l'est.
|
|
66
101
|
* @param env - environnement constaté.
|
|
102
|
+
* @param appHasMigrations - l'application versionne au moins une migration.
|
|
67
103
|
* @returns le mode effectif.
|
|
68
104
|
*/
|
|
69
|
-
export declare function resolveDdlMode(explicit: DdlMode | undefined, env: IMigrationEnv): DdlMode;
|
|
105
|
+
export declare function resolveDdlMode(explicit: DdlMode | undefined, env: IMigrationEnv, appHasMigrations?: boolean): DdlMode;
|
|
106
|
+
/**
|
|
107
|
+
* L'application versionne-t-elle au moins une migration ?
|
|
108
|
+
*
|
|
109
|
+
* Lecture SYNCHRONE et bornée à un `readdirSync` par connecteur au démarrage —
|
|
110
|
+
* hors de tout chemin de requête. Elle doit l'être : le mode de schéma se
|
|
111
|
+
* décide avant la connexion, et le rendre asynchrone propagerait un `await`
|
|
112
|
+
* dans toute la chaîne de résolution pour une réponse qui ne change jamais
|
|
113
|
+
* pendant la vie du processus.
|
|
114
|
+
*
|
|
115
|
+
* Un dossier absent, illisible ou vide répond `false` — l'application n'a alors
|
|
116
|
+
* rien à appliquer, et `auto` reste le bon défaut de démarrage rapide.
|
|
117
|
+
*
|
|
118
|
+
* @param dir - dossier de migrations de l'application ({@link appMigrationsDir}).
|
|
119
|
+
* @returns `true` si au moins un fichier `.sql` y est versionné, à un niveau
|
|
120
|
+
* ou sous un sous-dossier de dialecte (`migrations/postgres/0001_x.sql`).
|
|
121
|
+
*/
|
|
122
|
+
export declare function appVersionsMigrations(dir: string | undefined): boolean;
|
|
70
123
|
/**
|
|
71
124
|
* La conduite de la sonde de disponibilité face à un schéma en retard.
|
|
72
125
|
*
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordre de création des tables, et contraintes qu'il faut renoncer à poser.
|
|
3
|
+
*
|
|
4
|
+
* Module **pur** (aucune base, aucun Drizzle) : c'est la seule partie du DDL
|
|
5
|
+
* dérivé qui demande un raisonnement, donc la seule qui mérite d'être éprouvée
|
|
6
|
+
* sans serveur — un cycle entre entités ne se reproduit pas à la demande sur une
|
|
7
|
+
* base réelle.
|
|
8
|
+
*
|
|
9
|
+
* Une clé étrangère se déclare **dans** le `CREATE TABLE` : la table visée doit
|
|
10
|
+
* donc exister AVANT celle qui la désigne. Un ordre quelconque échoue sur la
|
|
11
|
+
* première relation rencontrée, et l'application ne démarre pas.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Une clé étrangère RÉSOLUE en noms SQL — prête à être écrite dans un
|
|
15
|
+
* `CREATE TABLE`, sans plus aucune référence à un objet Drizzle.
|
|
16
|
+
*/
|
|
17
|
+
export interface IResolvedForeignKey {
|
|
18
|
+
/** Nom de la contrainte — explicite, car MySQL l'exige unique pour la base. */
|
|
19
|
+
readonly name: string;
|
|
20
|
+
/** Colonnes porteuses, côté table courante. */
|
|
21
|
+
readonly columns: readonly string[];
|
|
22
|
+
/** Table visée. */
|
|
23
|
+
readonly foreignTable: string;
|
|
24
|
+
/** Colonnes visées, dans le même ordre que {@link columns}. */
|
|
25
|
+
readonly foreignColumns: readonly string[];
|
|
26
|
+
/** Politique d'effacement du parent (`restrict`, `set null`, `cascade`…). */
|
|
27
|
+
readonly onDelete?: string;
|
|
28
|
+
/** Politique de mise à jour de la clé du parent. */
|
|
29
|
+
readonly onUpdate?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Une table à créer, et les tables que ses clés étrangères désignent. */
|
|
32
|
+
export interface ITableDependency {
|
|
33
|
+
/** Nom SQL de la table. */
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/**
|
|
36
|
+
* Tables visées par ses clés étrangères, dans l'ordre de déclaration.
|
|
37
|
+
*
|
|
38
|
+
* Une table peut y figurer plusieurs fois (deux colonnes vers la même cible) ;
|
|
39
|
+
* elle peut aussi s'y désigner elle-même, ce qui ne crée aucune dépendance.
|
|
40
|
+
*/
|
|
41
|
+
readonly references: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
/** Une contrainte qu'on renonce à poser, et la raison qui le commande. */
|
|
44
|
+
export interface IOmittedForeignKey {
|
|
45
|
+
/** Table qui porte la clé étrangère. */
|
|
46
|
+
readonly table: string;
|
|
47
|
+
/** Table visée. */
|
|
48
|
+
readonly target: string;
|
|
49
|
+
/**
|
|
50
|
+
* `cycle` : la cible dépend à son tour de cette table, directement ou non —
|
|
51
|
+
* aucun ordre de création ne satisfait les deux.
|
|
52
|
+
* `absent` : la cible n'appartient pas à ce connecteur, donc à cette base.
|
|
53
|
+
*/
|
|
54
|
+
readonly reason: "cycle" | "absent";
|
|
55
|
+
}
|
|
56
|
+
/** Ordre de création, et ce qu'il a fallu abandonner en chemin. */
|
|
57
|
+
export interface IDdlPlan {
|
|
58
|
+
/** Noms des tables, dans l'ordre où les créer. */
|
|
59
|
+
readonly order: readonly string[];
|
|
60
|
+
/** Clés étrangères non émises — jamais silencieuses, l'appelant les journalise. */
|
|
61
|
+
readonly omitted: readonly IOmittedForeignKey[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Trie les tables pour que chaque cible naisse avant qui la désigne.
|
|
65
|
+
*
|
|
66
|
+
* Tri topologique (Kahn) sur le graphe des dépendances, avec deux écarts
|
|
67
|
+
* assumés au manuel :
|
|
68
|
+
*
|
|
69
|
+
* - **l'auto-référence ne compte pas** (`parent:ref:Category` dans `Category`) :
|
|
70
|
+
* les trois moteurs acceptent une contrainte qui vise la table en cours de
|
|
71
|
+
* création, dans le même `CREATE TABLE` ;
|
|
72
|
+
* - **un cycle ne fait pas échouer le démarrage**. Il n'y a aucun ordre correct,
|
|
73
|
+
* mais refuser de démarrer pour ça serait hors de proportion : on crée les
|
|
74
|
+
* tables du cycle dans un ordre stable, et on OMET les contraintes qui
|
|
75
|
+
* regardent en arrière. Elles sont rendues à l'appelant pour qu'il les dise —
|
|
76
|
+
* une garantie qui disparaît sans un mot est pire que son absence.
|
|
77
|
+
*
|
|
78
|
+
* À ordre de sortie stable : les tables prêtes sont prises par ordre
|
|
79
|
+
* alphabétique, jamais dans l'ordre d'arrivée. Deux démarrages de la même
|
|
80
|
+
* application produisent donc le même DDL, ce qui rend un journal comparable.
|
|
81
|
+
*
|
|
82
|
+
* @param tables - les tables du connecteur et leurs cibles.
|
|
83
|
+
* @returns l'ordre de création et les contraintes abandonnées.
|
|
84
|
+
*/
|
|
85
|
+
export declare function planTableCreation(tables: readonly ITableDependency[]): IDdlPlan;
|
|
86
|
+
/**
|
|
87
|
+
* Met en mots les contraintes abandonnées — le message que l'appelant journalise.
|
|
88
|
+
*
|
|
89
|
+
* Écrit pour être lu par celui qui découvre le problème dans ses journaux : il
|
|
90
|
+
* nomme les tables, la cause, et ce qu'il reste à faire. Un avertissement qui ne
|
|
91
|
+
* dit pas quoi faire se relit deux fois puis s'ignore.
|
|
92
|
+
*
|
|
93
|
+
* @param omitted - les contraintes non émises.
|
|
94
|
+
* @returns le texte à journaliser, ou `null` s'il n'y a rien à dire.
|
|
95
|
+
*/
|
|
96
|
+
export declare function explainOmittedForeignKeys(omitted: readonly IOmittedForeignKey[]): string | null;
|