@vibeorm/migrate 1.1.9 → 1.2.0
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/package.json +2 -2
- package/src/cascade-actions.ts +88 -0
- package/src/ddl-builder.ts +3 -1
- package/src/introspector.ts +63 -24
- package/src/schema-differ.ts +43 -11
- package/src/schema-printer.ts +39 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibeorm/migrate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Migration, introspection, and schema diff toolkit for VibeORM",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"bun": ">=1.1.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@vibeorm/parser": "1.
|
|
40
|
+
"@vibeorm/parser": "1.2.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cascade-action resolution and SQL serialisation.
|
|
3
|
+
*
|
|
4
|
+
* Translates a `CascadeAction` from the parser IR into a Postgres
|
|
5
|
+
* `ON DELETE`/`ON UPDATE` clause, applying Prisma-compatible defaults when
|
|
6
|
+
* the schema doesn't specify an action explicitly.
|
|
7
|
+
*
|
|
8
|
+
* Defaults (matching Prisma's Postgres behaviour):
|
|
9
|
+
* - `onDelete`: required FK → `Restrict`, optional FK → `SetNull`
|
|
10
|
+
* - `onUpdate`: always → `Cascade`
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { CascadeAction, RelationField } from "@vibeorm/parser";
|
|
14
|
+
|
|
15
|
+
const ACTION_TO_SQL: Record<CascadeAction, string> = {
|
|
16
|
+
Cascade: "CASCADE",
|
|
17
|
+
Restrict: "RESTRICT",
|
|
18
|
+
NoAction: "NO ACTION",
|
|
19
|
+
SetNull: "SET NULL",
|
|
20
|
+
SetDefault: "SET DEFAULT",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Inverse of {@link ACTION_TO_SQL} — used by the introspector when reading
|
|
25
|
+
* `pg_constraint.confdeltype`/`confupdtype`. The single-char codes are
|
|
26
|
+
* Postgres's catalog representation.
|
|
27
|
+
*/
|
|
28
|
+
export const PG_CHAR_TO_CASCADE_ACTION: Record<string, CascadeAction> = {
|
|
29
|
+
a: "NoAction",
|
|
30
|
+
r: "Restrict",
|
|
31
|
+
c: "Cascade",
|
|
32
|
+
n: "SetNull",
|
|
33
|
+
d: "SetDefault",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the effective onDelete action for a relation, applying the Prisma
|
|
38
|
+
* default when the schema didn't say anything explicit.
|
|
39
|
+
*
|
|
40
|
+
* `isRequired` is the FK column's nullability (Prisma's "required relation").
|
|
41
|
+
*/
|
|
42
|
+
export function resolveOnDelete(params: {
|
|
43
|
+
explicit: CascadeAction | undefined;
|
|
44
|
+
isRequired: boolean;
|
|
45
|
+
}): CascadeAction {
|
|
46
|
+
if (params.explicit) return params.explicit;
|
|
47
|
+
return params.isRequired ? "Restrict" : "SetNull";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Resolve the effective onUpdate action. Prisma's Postgres default is Cascade. */
|
|
51
|
+
export function resolveOnUpdate(params: {
|
|
52
|
+
explicit: CascadeAction | undefined;
|
|
53
|
+
}): CascadeAction {
|
|
54
|
+
return params.explicit ?? "Cascade";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Convenience: serialise a CascadeAction to its Postgres clause body. */
|
|
58
|
+
export function cascadeActionSql(params: { action: CascadeAction }): string {
|
|
59
|
+
return ACTION_TO_SQL[params.action];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the full `ON DELETE X ON UPDATE Y` clause for a relation field,
|
|
64
|
+
* applying defaults as needed.
|
|
65
|
+
*/
|
|
66
|
+
export function buildFkActionClause(params: { rel: RelationField }): string {
|
|
67
|
+
const { rel } = params;
|
|
68
|
+
const onDelete = resolveOnDelete({
|
|
69
|
+
explicit: rel.relation.onDelete,
|
|
70
|
+
isRequired: rel.isRequired,
|
|
71
|
+
});
|
|
72
|
+
const onUpdate = resolveOnUpdate({ explicit: rel.relation.onUpdate });
|
|
73
|
+
return `ON DELETE ${ACTION_TO_SQL[onDelete]} ON UPDATE ${ACTION_TO_SQL[onUpdate]}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Stable comparison key for whether two relations have the same effective
|
|
78
|
+
* cascade behaviour. Used by the differ to detect drift even when the schema
|
|
79
|
+
* was written without explicit actions (defaults must still be considered).
|
|
80
|
+
*/
|
|
81
|
+
export function fkActionFingerprint(params: { rel: RelationField }): string {
|
|
82
|
+
const onDelete = resolveOnDelete({
|
|
83
|
+
explicit: params.rel.relation.onDelete,
|
|
84
|
+
isRequired: params.rel.isRequired,
|
|
85
|
+
});
|
|
86
|
+
const onUpdate = resolveOnUpdate({ explicit: params.rel.relation.onUpdate });
|
|
87
|
+
return `${onDelete}|${onUpdate}`;
|
|
88
|
+
}
|
package/src/ddl-builder.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { Schema, Model, Field, ScalarField, EnumField, RelationField, DefaultValue, Enum, EnumValue } from "@vibeorm/parser";
|
|
9
9
|
import { PRISMA_TO_PG } from "@vibeorm/parser";
|
|
10
|
+
import { buildFkActionClause } from "./cascade-actions.ts";
|
|
10
11
|
|
|
11
12
|
export type DDLResult = {
|
|
12
13
|
/** The full DDL SQL wrapped in BEGIN/COMMIT */
|
|
@@ -271,10 +272,11 @@ function buildForeignKeyStatements(params: { model: Model; allModels: Model[] })
|
|
|
271
272
|
.join(", ");
|
|
272
273
|
|
|
273
274
|
const constraintName = `${tableName}_${field.relation.fields.join("_")}_fkey`;
|
|
275
|
+
const actionClause = buildFkActionClause({ rel: field });
|
|
274
276
|
statements.push(
|
|
275
277
|
`ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} ` +
|
|
276
278
|
`FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ` +
|
|
277
|
-
|
|
279
|
+
`${actionClause};`
|
|
278
280
|
);
|
|
279
281
|
}
|
|
280
282
|
|
package/src/introspector.ts
CHANGED
|
@@ -20,9 +20,11 @@ import type {
|
|
|
20
20
|
PrismaScalarType,
|
|
21
21
|
PostgresType,
|
|
22
22
|
DefaultValue,
|
|
23
|
+
CascadeAction,
|
|
23
24
|
} from "@vibeorm/parser";
|
|
24
25
|
import { PRISMA_TO_TS, PRISMA_TO_PG } from "@vibeorm/parser";
|
|
25
26
|
import type { SqlExecutor } from "./types.ts";
|
|
27
|
+
import { PG_CHAR_TO_CASCADE_ACTION } from "./cascade-actions.ts";
|
|
26
28
|
|
|
27
29
|
// ─── Reverse Type Mapping ─────────────────────────────────────────
|
|
28
30
|
|
|
@@ -83,6 +85,10 @@ type ForeignKeyInfo = {
|
|
|
83
85
|
columnName: string;
|
|
84
86
|
foreignTableName: string;
|
|
85
87
|
foreignColumnName: string;
|
|
88
|
+
/** ON DELETE action from `pg_constraint.confdeltype` (mapped to CascadeAction). */
|
|
89
|
+
onDelete: CascadeAction;
|
|
90
|
+
/** ON UPDATE action from `pg_constraint.confupdtype` (mapped to CascadeAction). */
|
|
91
|
+
onUpdate: CascadeAction;
|
|
86
92
|
};
|
|
87
93
|
|
|
88
94
|
type EnumInfo = {
|
|
@@ -138,23 +144,36 @@ const CONSTRAINTS_QUERY = `
|
|
|
138
144
|
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position;
|
|
139
145
|
`;
|
|
140
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Foreign keys via pg_catalog rather than information_schema, because
|
|
149
|
+
* information_schema's `referential_constraints.delete_rule` exposes the rule
|
|
150
|
+
* but doesn't preserve the column ordering of composite FKs reliably across
|
|
151
|
+
* server versions. The pg_catalog path with `unnest(...) WITH ORDINALITY`
|
|
152
|
+
* pairs up `conkey` / `confkey` in declaration order and also surfaces
|
|
153
|
+
* `confdeltype` / `confupdtype` directly.
|
|
154
|
+
*/
|
|
141
155
|
const FOREIGN_KEYS_QUERY = `
|
|
142
156
|
SELECT
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
JOIN
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
c.conname AS constraint_name,
|
|
158
|
+
cl.relname AS table_name,
|
|
159
|
+
att.attname AS column_name,
|
|
160
|
+
fcl.relname AS foreign_table_name,
|
|
161
|
+
fatt.attname AS foreign_column_name,
|
|
162
|
+
c.confdeltype AS on_delete_char,
|
|
163
|
+
c.confupdtype AS on_update_char
|
|
164
|
+
FROM pg_catalog.pg_constraint c
|
|
165
|
+
JOIN pg_catalog.pg_class cl ON cl.oid = c.conrelid
|
|
166
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace
|
|
167
|
+
JOIN pg_catalog.pg_class fcl ON fcl.oid = c.confrelid
|
|
168
|
+
JOIN pg_catalog.pg_namespace fn ON fn.oid = fcl.relnamespace
|
|
169
|
+
JOIN unnest(c.conkey) WITH ORDINALITY AS lk(attnum, ord) ON true
|
|
170
|
+
JOIN unnest(c.confkey) WITH ORDINALITY AS rk(attnum, ord) ON lk.ord = rk.ord
|
|
171
|
+
JOIN pg_catalog.pg_attribute att ON att.attrelid = c.conrelid AND att.attnum = lk.attnum
|
|
172
|
+
JOIN pg_catalog.pg_attribute fatt ON fatt.attrelid = c.confrelid AND fatt.attnum = rk.attnum
|
|
173
|
+
WHERE c.contype = 'f'
|
|
174
|
+
AND n.nspname = 'public'
|
|
175
|
+
AND fn.nspname = 'public'
|
|
176
|
+
ORDER BY cl.relname, c.conname, lk.ord;
|
|
158
177
|
`;
|
|
159
178
|
|
|
160
179
|
const ENUMS_QUERY = `
|
|
@@ -289,13 +308,22 @@ export async function introspect(params: { executor: SqlExecutor }): Promise<Sch
|
|
|
289
308
|
columnName: r.column_name as string,
|
|
290
309
|
}));
|
|
291
310
|
|
|
292
|
-
const foreignKeys: ForeignKeyInfo[] = foreignKeysRaw.map((r) =>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
311
|
+
const foreignKeys: ForeignKeyInfo[] = foreignKeysRaw.map((r) => {
|
|
312
|
+
// Postgres uses single-char codes: a=NoAction, r=Restrict, c=Cascade,
|
|
313
|
+
// n=SetNull, d=SetDefault. Fall back to NoAction (the Postgres default)
|
|
314
|
+
// if we ever see an unrecognised char from a future PG version.
|
|
315
|
+
const onDeleteChar = (r.on_delete_char as string) ?? "a";
|
|
316
|
+
const onUpdateChar = (r.on_update_char as string) ?? "a";
|
|
317
|
+
return {
|
|
318
|
+
constraintName: r.constraint_name as string,
|
|
319
|
+
tableName: r.table_name as string,
|
|
320
|
+
columnName: r.column_name as string,
|
|
321
|
+
foreignTableName: r.foreign_table_name as string,
|
|
322
|
+
foreignColumnName: r.foreign_column_name as string,
|
|
323
|
+
onDelete: PG_CHAR_TO_CASCADE_ACTION[onDeleteChar] ?? "NoAction",
|
|
324
|
+
onUpdate: PG_CHAR_TO_CASCADE_ACTION[onUpdateChar] ?? "NoAction",
|
|
325
|
+
};
|
|
326
|
+
});
|
|
299
327
|
|
|
300
328
|
const enumInfos: EnumInfo[] = enumsRaw.map((r) => ({
|
|
301
329
|
typeName: r.type_name as string,
|
|
@@ -387,14 +415,23 @@ export async function introspect(params: { executor: SqlExecutor }): Promise<Sch
|
|
|
387
415
|
const tableUniques = uniqueConstraints.get(tableName) ?? new Map<string, string[]>();
|
|
388
416
|
const tableFKs = foreignKeys.filter((fk) => fk.tableName === tableName);
|
|
389
417
|
|
|
390
|
-
// Group FKs by constraint for multi-column FK support
|
|
391
|
-
|
|
418
|
+
// Group FKs by constraint for multi-column FK support. Cascade actions are
|
|
419
|
+
// a property of the constraint as a whole, so we carry them once per group.
|
|
420
|
+
const fkGroups = new Map<string, {
|
|
421
|
+
localCols: string[];
|
|
422
|
+
foreignTable: string;
|
|
423
|
+
foreignCols: string[];
|
|
424
|
+
onDelete: CascadeAction;
|
|
425
|
+
onUpdate: CascadeAction;
|
|
426
|
+
}>();
|
|
392
427
|
for (const fk of tableFKs) {
|
|
393
428
|
if (!fkGroups.has(fk.constraintName)) {
|
|
394
429
|
fkGroups.set(fk.constraintName, {
|
|
395
430
|
localCols: [],
|
|
396
431
|
foreignTable: fk.foreignTableName,
|
|
397
432
|
foreignCols: [],
|
|
433
|
+
onDelete: fk.onDelete,
|
|
434
|
+
onUpdate: fk.onUpdate,
|
|
398
435
|
});
|
|
399
436
|
}
|
|
400
437
|
const group = fkGroups.get(fk.constraintName)!;
|
|
@@ -473,6 +510,8 @@ export async function introspect(params: { executor: SqlExecutor }): Promise<Sch
|
|
|
473
510
|
relatedModel: group.foreignTable,
|
|
474
511
|
type: isOneToOne ? "oneToOne" : "manyToOne",
|
|
475
512
|
isForeignKey: true,
|
|
513
|
+
onDelete: group.onDelete,
|
|
514
|
+
onUpdate: group.onUpdate,
|
|
476
515
|
},
|
|
477
516
|
};
|
|
478
517
|
fields.push(relationField);
|
package/src/schema-differ.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
IndexDefinition,
|
|
18
18
|
} from "@vibeorm/parser";
|
|
19
19
|
import { PRISMA_TO_PG } from "@vibeorm/parser";
|
|
20
|
+
import { buildFkActionClause, fkActionFingerprint } from "./cascade-actions.ts";
|
|
20
21
|
|
|
21
22
|
// ─── Types ────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -426,9 +427,10 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
426
427
|
}).join(", ");
|
|
427
428
|
const resolvedFkNames = rel.relation.fields.map((f: string) => resolveFieldDbName({ model, fieldName: f }));
|
|
428
429
|
const constraintName = `${tableName}_${resolvedFkNames.join("_")}_fkey`;
|
|
430
|
+
const actionClause = buildFkActionClause({ rel });
|
|
429
431
|
ops.push({
|
|
430
432
|
type: "addForeignKey",
|
|
431
|
-
sql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols})
|
|
433
|
+
sql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ${actionClause};`,
|
|
432
434
|
downSql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
|
|
433
435
|
isDestructive: false,
|
|
434
436
|
isReversible: true,
|
|
@@ -709,7 +711,7 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
709
711
|
);
|
|
710
712
|
|
|
711
713
|
for (const [key, rel] of currFKs) {
|
|
712
|
-
|
|
714
|
+
const prev = prevFKs.get(key);
|
|
713
715
|
const fkCols = rel.relation.fields.map((f: string) => q({ name: resolveFieldDbName({ model: currModel, fieldName: f }) })).join(", ");
|
|
714
716
|
const relatedModel = current.models.find((m: Model) => m.name === rel.relatedModel);
|
|
715
717
|
const relatedTableName = relatedModel?.dbName ?? rel.relatedModel;
|
|
@@ -719,14 +721,43 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
719
721
|
}).join(", ");
|
|
720
722
|
const resolvedFkNames = rel.relation.fields.map((f: string) => resolveFieldDbName({ model: currModel, fieldName: f }));
|
|
721
723
|
const constraintName = `${tableName}_${resolvedFkNames.join("_")}_fkey`;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
724
|
+
const actionClause = buildFkActionClause({ rel });
|
|
725
|
+
|
|
726
|
+
if (!prev) {
|
|
727
|
+
ops.push({
|
|
728
|
+
type: "addForeignKey",
|
|
729
|
+
sql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ${actionClause};`,
|
|
730
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
|
|
731
|
+
isDestructive: false,
|
|
732
|
+
isReversible: true,
|
|
733
|
+
description: `Add foreign key on "${name}" (${rel.relation.fields.join(", ")}) → "${rel.relatedModel}"`,
|
|
734
|
+
});
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// FK exists on both sides — detect cascade-action drift. Postgres has no
|
|
739
|
+
// `ALTER CONSTRAINT ... ON DELETE` for FKs, so we emit a DROP + ADD pair.
|
|
740
|
+
const prevFingerprint = fkActionFingerprint({ rel: prev });
|
|
741
|
+
const currFingerprint = fkActionFingerprint({ rel });
|
|
742
|
+
if (prevFingerprint !== currFingerprint) {
|
|
743
|
+
const prevActionClause = buildFkActionClause({ rel: prev });
|
|
744
|
+
ops.push({
|
|
745
|
+
type: "dropForeignKey",
|
|
746
|
+
sql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
|
|
747
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ${prevActionClause};`,
|
|
748
|
+
isDestructive: false,
|
|
749
|
+
isReversible: true,
|
|
750
|
+
description: `Drop foreign key on "${name}" (${rel.relation.fields.join(", ")}) to change cascade actions`,
|
|
751
|
+
});
|
|
752
|
+
ops.push({
|
|
753
|
+
type: "addForeignKey",
|
|
754
|
+
sql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ${actionClause};`,
|
|
755
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
|
|
756
|
+
isDestructive: false,
|
|
757
|
+
isReversible: true,
|
|
758
|
+
description: `Re-add foreign key on "${name}" (${rel.relation.fields.join(", ")}) with new cascade actions (${currFingerprint.replace("|", " / ")})`,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
730
761
|
}
|
|
731
762
|
|
|
732
763
|
for (const [key, rel] of prevFKs) {
|
|
@@ -740,10 +771,11 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
|
|
|
740
771
|
}).join(", ");
|
|
741
772
|
const resolvedFkNames = rel.relation.fields.map((f: string) => resolveFieldDbName({ model: prevModel, fieldName: f }));
|
|
742
773
|
const constraintName = `${tableName}_${resolvedFkNames.join("_")}_fkey`;
|
|
774
|
+
const actionClause = buildFkActionClause({ rel });
|
|
743
775
|
ops.push({
|
|
744
776
|
type: "dropForeignKey",
|
|
745
777
|
sql: `ALTER TABLE ${q({ name: tableName })} DROP CONSTRAINT IF EXISTS ${q({ name: constraintName })};`,
|
|
746
|
-
downSql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols})
|
|
778
|
+
downSql: `ALTER TABLE ${q({ name: tableName })} ADD CONSTRAINT ${q({ name: constraintName })} FOREIGN KEY (${fkCols}) REFERENCES ${q({ name: relatedTableName })} (${refCols}) ${actionClause};`,
|
|
747
779
|
isDestructive: true,
|
|
748
780
|
isReversible: true,
|
|
749
781
|
description: `Drop foreign key on "${name}" (${rel.relation.fields.join(", ")}) → "${rel.relatedModel}"`,
|
package/src/schema-printer.ts
CHANGED
|
@@ -14,7 +14,9 @@ import type {
|
|
|
14
14
|
RelationField,
|
|
15
15
|
Enum,
|
|
16
16
|
DefaultValue,
|
|
17
|
+
CascadeAction,
|
|
17
18
|
} from "@vibeorm/parser";
|
|
19
|
+
import { resolveOnDelete, resolveOnUpdate } from "./cascade-actions.ts";
|
|
18
20
|
|
|
19
21
|
// ─── Helpers ──────────────────────────────────────────────────────
|
|
20
22
|
|
|
@@ -105,12 +107,21 @@ function printRelationField(params: { field: RelationField }): string {
|
|
|
105
107
|
if (field.relation.isForeignKey && field.relation.fields.length > 0) {
|
|
106
108
|
const fieldsStr = field.relation.fields.map((f: string) => f).join(", ");
|
|
107
109
|
const refsStr = field.relation.references.map((f: string) => f).join(", ");
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
|
|
111
|
+
// Only emit cascade actions when they differ from the Prisma default for
|
|
112
|
+
// this relation's optionality. Keeps `db pull` output minimal and matches
|
|
113
|
+
// what users would have hand-written.
|
|
114
|
+
const explicitOnDelete = nonDefaultOnDelete({ field });
|
|
115
|
+
const explicitOnUpdate = nonDefaultOnUpdate({ field });
|
|
116
|
+
|
|
117
|
+
const segments: string[] = [];
|
|
118
|
+
if (field.relation.name) segments.push(`"${field.relation.name}"`);
|
|
119
|
+
segments.push(`fields: [${fieldsStr}]`);
|
|
120
|
+
segments.push(`references: [${refsStr}]`);
|
|
121
|
+
if (explicitOnDelete) segments.push(`onDelete: ${explicitOnDelete}`);
|
|
122
|
+
if (explicitOnUpdate) segments.push(`onUpdate: ${explicitOnUpdate}`);
|
|
123
|
+
|
|
124
|
+
parts.push(`@relation(${segments.join(", ")})`);
|
|
114
125
|
} else if (field.relation.name) {
|
|
115
126
|
parts.push(`@relation("${field.relation.name}")`);
|
|
116
127
|
}
|
|
@@ -118,6 +129,28 @@ function printRelationField(params: { field: RelationField }): string {
|
|
|
118
129
|
return indent({ text: parts.join(" ") });
|
|
119
130
|
}
|
|
120
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Returns the onDelete action only if it differs from the Prisma default for
|
|
134
|
+
* this relation's optionality. Returns `undefined` when the action is the
|
|
135
|
+
* default (in which case we omit it from the printed `@relation(...)`).
|
|
136
|
+
*/
|
|
137
|
+
function nonDefaultOnDelete(params: { field: RelationField }): CascadeAction | undefined {
|
|
138
|
+
const { field } = params;
|
|
139
|
+
if (field.relation.onDelete === undefined) return undefined;
|
|
140
|
+
const effectiveDefault = resolveOnDelete({
|
|
141
|
+
explicit: undefined,
|
|
142
|
+
isRequired: field.isRequired,
|
|
143
|
+
});
|
|
144
|
+
return field.relation.onDelete === effectiveDefault ? undefined : field.relation.onDelete;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function nonDefaultOnUpdate(params: { field: RelationField }): CascadeAction | undefined {
|
|
148
|
+
const { field } = params;
|
|
149
|
+
if (field.relation.onUpdate === undefined) return undefined;
|
|
150
|
+
const effectiveDefault = resolveOnUpdate({ explicit: undefined });
|
|
151
|
+
return field.relation.onUpdate === effectiveDefault ? undefined : field.relation.onUpdate;
|
|
152
|
+
}
|
|
153
|
+
|
|
121
154
|
function printField(params: { field: Field }): string {
|
|
122
155
|
switch (params.field.kind) {
|
|
123
156
|
case "scalar":
|