@prisma-next/family-sql 0.14.0 → 0.15.0-dev.10

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 (80) hide show
  1. package/dist/{authoring-type-constructors-CjFfO6LM.mjs → authoring-type-constructors-CXd-8ydc.mjs} +7 -30
  2. package/dist/authoring-type-constructors-CXd-8ydc.mjs.map +1 -0
  3. package/dist/{control-adapter-Cmw9LvEP.d.mts → control-adapter-BQgad8Zc.d.mts} +30 -8
  4. package/dist/control-adapter-BQgad8Zc.d.mts.map +1 -0
  5. package/dist/control-adapter.d.mts +1 -1
  6. package/dist/control.d.mts +279 -34
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +414 -1005
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/diff.d.mts +102 -0
  11. package/dist/diff.d.mts.map +1 -0
  12. package/dist/diff.mjs +14 -0
  13. package/dist/diff.mjs.map +1 -0
  14. package/dist/ir.d.mts +26 -9
  15. package/dist/ir.d.mts.map +1 -1
  16. package/dist/ir.mjs +2 -2
  17. package/dist/ir.mjs.map +1 -1
  18. package/dist/migration.d.mts +11 -2
  19. package/dist/migration.d.mts.map +1 -1
  20. package/dist/migration.mjs +7 -0
  21. package/dist/migration.mjs.map +1 -1
  22. package/dist/pack.mjs +1 -1
  23. package/dist/psl-infer.d.mts +119 -0
  24. package/dist/psl-infer.d.mts.map +1 -0
  25. package/dist/psl-infer.mjs +366 -0
  26. package/dist/psl-infer.mjs.map +1 -0
  27. package/dist/schema-differ-DnoopSXm.d.mts +45 -0
  28. package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
  29. package/dist/schema-verify-3sOPsdn_.mjs +226 -0
  30. package/dist/schema-verify-3sOPsdn_.mjs.map +1 -0
  31. package/dist/{sql-contract-serializer-BR2vC7Z-.mjs → sql-contract-serializer-C75cfMSS.mjs} +46 -20
  32. package/dist/sql-contract-serializer-C75cfMSS.mjs.map +1 -0
  33. package/dist/{types-kgstZ_Zd.d.mts → types-BPv_y7iS.d.mts} +36 -142
  34. package/dist/types-BPv_y7iS.d.mts.map +1 -0
  35. package/package.json +26 -26
  36. package/src/core/authoring-entity-types.ts +12 -37
  37. package/src/core/control-adapter.ts +11 -5
  38. package/src/core/control-instance.ts +212 -42
  39. package/src/core/control-target-descriptor.ts +98 -0
  40. package/src/core/diff/schema-verify.ts +324 -0
  41. package/src/core/diff/sql-schema-diff.ts +41 -0
  42. package/src/core/diff/verifier-disposition.ts +30 -0
  43. package/src/core/ir/sql-contract-serializer-base.ts +70 -56
  44. package/src/core/ir/sql-contract-serializer.ts +5 -7
  45. package/src/core/ir/sql-schema-verifier-base.ts +5 -5
  46. package/src/core/migrations/contract-to-schema-ir.ts +200 -54
  47. package/src/core/migrations/control-policy.ts +66 -115
  48. package/src/core/migrations/field-event-planner.ts +2 -2
  49. package/src/core/migrations/native-type-expander.ts +28 -0
  50. package/src/core/migrations/schema-differ.ts +41 -0
  51. package/src/core/migrations/types.ts +33 -37
  52. package/src/core/psl-contract-infer/name-transforms.ts +15 -0
  53. package/src/core/psl-contract-infer/printer-config.ts +14 -6
  54. package/src/core/psl-contract-infer/relation-inference.ts +30 -1
  55. package/src/core/sql-migration.ts +12 -1
  56. package/src/exports/control.ts +12 -2
  57. package/src/exports/diff.ts +25 -0
  58. package/src/exports/psl-infer.ts +40 -0
  59. package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +0 -1
  60. package/dist/control-adapter-Cmw9LvEP.d.mts.map +0 -1
  61. package/dist/schema-verify.d.mts +0 -39
  62. package/dist/schema-verify.d.mts.map +0 -1
  63. package/dist/schema-verify.mjs +0 -2
  64. package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +0 -1
  65. package/dist/test-utils.d.mts +0 -2
  66. package/dist/test-utils.mjs +0 -2
  67. package/dist/types-kgstZ_Zd.d.mts.map +0 -1
  68. package/dist/verify-sql-schema-thU-jKpf.d.mts +0 -66
  69. package/dist/verify-sql-schema-thU-jKpf.d.mts.map +0 -1
  70. package/dist/verify-sql-schema-xT4udQLQ.mjs +0 -1501
  71. package/dist/verify-sql-schema-xT4udQLQ.mjs.map +0 -1
  72. package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
  73. package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
  74. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
  75. package/src/core/schema-verify/control-verify-emit.ts +0 -46
  76. package/src/core/schema-verify/verifier-disposition.ts +0 -58
  77. package/src/core/schema-verify/verify-helpers.ts +0 -820
  78. package/src/core/schema-verify/verify-sql-schema.ts +0 -1311
  79. package/src/exports/schema-verify.ts +0 -18
  80. package/src/exports/test-utils.ts +0 -9
@@ -1,5 +1,9 @@
1
1
  import type { ColumnDefault, Contract, JsonValue } from '@prisma-next/contract/types';
2
- import type { MigrationPlannerConflict } from '@prisma-next/framework-components/control';
2
+ import type { CodecRef } from '@prisma-next/framework-components/codec';
3
+ import type {
4
+ MigrationPlannerConflict,
5
+ SchemaNodeRef,
6
+ } from '@prisma-next/framework-components/control';
3
7
  import {
4
8
  type CheckConstraint,
5
9
  type ForeignKey,
@@ -11,17 +15,18 @@ import {
11
15
  type StorageTypeInstance,
12
16
  type UniqueConstraint,
13
17
  } from '@prisma-next/sql-contract/types';
14
- import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
15
- import type {
16
- SqlAnnotations,
17
- SqlCheckConstraintIRInput,
18
- SqlColumnIR,
19
- SqlForeignKeyIR,
20
- SqlIndexIR,
18
+ import {
19
+ RelationalSchemaNodeKind,
20
+ type SqlAnnotations,
21
+ type SqlCheckConstraintIRInput,
22
+ type SqlColumnIRInput,
23
+ type SqlForeignKeyIRInput,
24
+ type SqlIndexIRInput,
21
25
  SqlSchemaIR,
22
26
  SqlTableIR,
23
- SqlUniqueIR,
27
+ type SqlUniqueIRInput,
24
28
  } from '@prisma-next/sql-schema-ir/types';
29
+ import { blindCast } from '@prisma-next/utils/casts';
25
30
  import { ifDefined } from '@prisma-next/utils/defined';
26
31
 
27
32
  /**
@@ -72,7 +77,7 @@ function convertColumn(
72
77
  storageTypes: ResolvedStorageTypes,
73
78
  expandNativeType: NativeTypeExpander | undefined,
74
79
  renderDefault: DefaultRenderer | undefined,
75
- ): SqlColumnIR {
80
+ ): SqlColumnIRInput {
76
81
  // Resolve `typeRef` so columns that delegate their `nativeType`/`codecId`/
77
82
  // `typeParams` to a named `storage.types` entry expand the same way as
78
83
  // columns that inline those fields. Without this resolution, a
@@ -82,21 +87,72 @@ function convertColumn(
82
87
  // typeRef and produces `"vector(1536)"` — making diffs on the same
83
88
  // contract falsely report a `type_mismatch`.
84
89
  const resolved = resolveColumnTypeMetadata(column, storageTypes);
85
- const nativeType = expandNativeType
90
+ const baseNativeType = expandNativeType
86
91
  ? expandNativeType({
87
92
  nativeType: resolved.nativeType,
88
93
  codecId: resolved.codecId,
89
94
  ...ifDefined('typeParams', resolved.typeParams),
90
95
  })
91
96
  : resolved.nativeType;
97
+ // `many: true` columns keep `nativeType` as the bare element type (matching
98
+ // how the introspected/"actual" side reports it — see the postgres control
99
+ // adapter's own `many`-stripping normalization) and carry the array-ness in
100
+ // the separate `many` field instead of baking `[]` into `nativeType`.
101
+ // Baking it in here (`"text[]"`) made every `@@` list-typed column
102
+ // permanently mismatch against a live introspected column reporting
103
+ // `{ nativeType: "text", many: true }`, regardless of whether the two are
104
+ // otherwise identical — `db verify` reported every such column
105
+ // `not-equal`. `resolvedNativeType` still carries the full `"text[]"`
106
+ // form: it is the side both this and the introspected column already
107
+ // agree on as the comparable "expanded" type.
108
+ const nativeType = baseNativeType;
109
+ const resolvedNativeType = column.many ? `${baseNativeType}[]` : baseNativeType;
92
110
  return {
93
111
  name,
94
112
  nativeType,
95
113
  nullable: column.nullable,
114
+ ...ifDefined('many', column.many),
96
115
  ...ifDefined(
97
116
  'default',
98
117
  column.default != null && renderDefault ? renderDefault(column.default, column) : undefined,
99
118
  ),
119
+ // Contract-derived columns are resolved by construction: the computed
120
+ // full native type doubles as the resolved value, and the contract's
121
+ // structured default is the resolved default (the introspected side
122
+ // stamps its normalizer's parse of the raw expression).
123
+ resolvedNativeType,
124
+ ...ifDefined('resolvedDefault', column.default ?? undefined),
125
+ // The column's codec identity, carried the same way the query AST
126
+ // carries `CodecRef` (TML-2456) — the migration planner's op-builders
127
+ // resolve DDL rendering from this at plan time (Decision 5), instead of
128
+ // reading a derivation-precomputed render payload.
129
+ codecRef: buildColumnCodecRef(resolved, column.many),
130
+ codecBaseNativeType: resolved.nativeType,
131
+ ...(column.typeRef !== undefined ? { codecNamedType: true } : {}),
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Builds the column's `CodecRef` from its resolved (post-`typeRef`) codec
137
+ * identity — the same construction the query AST and the migration DDL
138
+ * renderer already use (TML-2456, TML-2918).
139
+ */
140
+ function buildColumnCodecRef(
141
+ resolved: Pick<StorageColumn, 'codecId' | 'nativeType' | 'typeParams'>,
142
+ many: boolean | undefined,
143
+ ): CodecRef {
144
+ return {
145
+ codecId: resolved.codecId,
146
+ ...ifDefined(
147
+ 'typeParams',
148
+ resolved.typeParams !== undefined
149
+ ? blindCast<
150
+ JsonValue,
151
+ 'resolved.typeParams is JsonValue-shaped storage metadata; the narrowed (non-undefined) value lands in CodecRef.typeParams which is JsonValue'
152
+ >(resolved.typeParams)
153
+ : undefined,
154
+ ),
155
+ ...ifDefined('many', many),
100
156
  };
101
157
  }
102
158
 
@@ -192,30 +248,88 @@ function convertCheck(check: CheckConstraint, storage: SqlStorage): SqlCheckCons
192
248
  };
193
249
  }
194
250
 
195
- function convertUnique(unique: UniqueConstraint): SqlUniqueIR {
251
+ function convertUnique(unique: UniqueConstraint, tableName: string): SqlUniqueIRInput {
196
252
  return {
197
253
  columns: unique.columns,
198
254
  ...ifDefined('name', unique.name),
255
+ dependsOn: flatColumnDependsOn(tableName, unique.columns),
199
256
  };
200
257
  }
201
258
 
202
- function convertIndex(index: Index): SqlIndexIR {
259
+ function convertIndex(index: Index, tableName: string): SqlIndexIRInput {
203
260
  return {
204
261
  columns: index.columns,
205
262
  unique: false,
206
263
  ...ifDefined('name', index.name),
264
+ // Carried so the derived index node compares type/options against the
265
+ // introspected side (the legacy walk read them from the contract).
266
+ ...ifDefined('type', index.type),
267
+ ...ifDefined('options', index.options),
268
+ dependsOn: flatColumnDependsOn(tableName, index.columns),
207
269
  };
208
270
  }
209
271
 
210
- function convertForeignKey(fk: ForeignKey): SqlForeignKeyIR {
272
+ /**
273
+ * The referenced table's chain in the flat (single-schema) tree
274
+ * `contractToSchemaIR`/`contractNamespaceToSchemaIR` build: the root
275
+ * (`SqlSchemaIR`, fixed `'database'` id) followed by the table's own id.
276
+ * Postgres discards this when it re-derives the FK against its own
277
+ * multi-schema tree shape (`contractToPostgresDatabaseSchemaNode`); SQLite's
278
+ * flat tree uses it as-is.
279
+ */
280
+ function flatSchemaDependsOn(tableName: string): SchemaNodeRef {
281
+ return [
282
+ { nodeKind: RelationalSchemaNodeKind.schema, id: 'database' },
283
+ { nodeKind: RelationalSchemaNodeKind.table, id: tableName },
284
+ ];
285
+ }
286
+
287
+ /**
288
+ * The chains from a table-child object (foreign key, index, unique, primary
289
+ * key) to each of the own columns it is built on, in the flat tree. Dropping
290
+ * a covered column auto-drops the object, so the object's drop must precede
291
+ * the column's; the graph derives that direction from these edges.
292
+ */
293
+ function flatColumnDependsOn(
294
+ tableName: string,
295
+ columns: readonly string[],
296
+ ): readonly SchemaNodeRef[] {
297
+ return columns.map((column) => [
298
+ { nodeKind: RelationalSchemaNodeKind.schema, id: 'database' },
299
+ { nodeKind: RelationalSchemaNodeKind.table, id: tableName },
300
+ { nodeKind: RelationalSchemaNodeKind.column, id: `column:${column}` },
301
+ ]);
302
+ }
303
+
304
+ /**
305
+ * The FK's referenced-namespace identity comes from the target's namespace
306
+ * node, not the raw namespace-id string. An unbound target namespace stamps
307
+ * no `referencedSchema` at all — the FK node's id renders the absence as the
308
+ * empty segment, which is what flat (single-schema) introspection produces,
309
+ * so both diff sides' FK ids meet by construction. A bound namespace (or a
310
+ * cross-space target whose namespace lives in another contract's storage)
311
+ * stamps its coordinate verbatim; namespaced targets (Postgres) resolve the
312
+ * real DDL schema downstream.
313
+ *
314
+ * `dependsOn` carries the referenced table (created before the FK, dropped
315
+ * after it) plus the FK's own columns (dropped after the FK, since dropping a
316
+ * column auto-drops the FK built on it).
317
+ */
318
+ function convertForeignKey(fk: ForeignKey, storage: SqlStorage): SqlForeignKeyIRInput {
319
+ const targetNamespace = storage.namespaces[fk.target.namespaceId];
320
+ const targetIsUnbound = targetNamespace?.isUnbound === true;
211
321
  return {
212
322
  columns: fk.source.columns,
213
323
  referencedTable: fk.target.tableName,
214
- referencedSchema: fk.target.namespaceId,
324
+ ...(targetIsUnbound ? {} : { referencedSchema: fk.target.namespaceId }),
215
325
  referencedColumns: fk.target.columns,
216
326
  ...ifDefined('name', fk.name),
217
327
  ...ifDefined('onDelete', fk.onDelete),
218
328
  ...ifDefined('onUpdate', fk.onUpdate),
329
+ dependsOn: [
330
+ flatSchemaDependsOn(fk.target.tableName),
331
+ ...flatColumnDependsOn(fk.source.tableName, fk.source.columns),
332
+ ],
219
333
  };
220
334
  }
221
335
 
@@ -227,7 +341,7 @@ function convertTable(
227
341
  renderDefault: DefaultRenderer | undefined,
228
342
  storage: SqlStorage,
229
343
  ): SqlTableIR {
230
- const columns: Record<string, SqlColumnIR> = {};
344
+ const columns: Record<string, SqlColumnIRInput> = {};
231
345
  for (const [colName, colDef] of Object.entries(table.columns)) {
232
346
  columns[colName] = convertColumn(
233
347
  colName,
@@ -238,38 +352,34 @@ function convertTable(
238
352
  );
239
353
  }
240
354
 
241
- const satisfiedIndexColumns = new Set([
242
- ...table.indexes.map((idx) => idx.columns.join(',')),
243
- ...table.uniques.map((unique) => unique.columns.join(',')),
244
- ...(table.primaryKey ? [table.primaryKey.columns.join(',')] : []),
245
- ]);
246
- const fkBackingIndexes: SqlIndexIR[] = [];
247
- for (const fk of table.foreignKeys) {
248
- if (fk.index === false) continue;
249
- const key = fk.source.columns.join(',');
250
- if (satisfiedIndexColumns.has(key)) continue;
251
- fkBackingIndexes.push({
252
- columns: fk.source.columns,
253
- unique: false,
254
- name: defaultIndexName(name, fk.source.columns),
255
- });
256
- satisfiedIndexColumns.add(key);
257
- }
258
-
259
355
  const checks: SqlCheckConstraintIRInput[] | undefined =
260
356
  table.checks && table.checks.length > 0
261
357
  ? table.checks.map((c) => convertCheck(c, storage))
262
358
  : undefined;
263
359
 
264
- return {
360
+ const primaryKey =
361
+ table.primaryKey !== undefined
362
+ ? {
363
+ columns: table.primaryKey.columns,
364
+ ...ifDefined('name', table.primaryKey.name),
365
+ dependsOn: flatColumnDependsOn(name, table.primaryKey.columns),
366
+ }
367
+ : undefined;
368
+
369
+ return new SqlTableIR({
265
370
  name,
266
371
  columns,
267
- ...ifDefined('primaryKey', table.primaryKey),
268
- foreignKeys: table.foreignKeys.filter((fk) => fk.constraint !== false).map(convertForeignKey),
269
- uniques: table.uniques.map(convertUnique),
270
- indexes: [...table.indexes.map(convertIndex), ...fkBackingIndexes],
372
+ ...ifDefined('primaryKey', primaryKey),
373
+ // #989 persists a `constraint: false` FK's absence and its backing index as
374
+ // discrete entities at contract construction, so every `foreignKeys[]` entry
375
+ // is now constraint-bearing (no filter) and each FK-backing index is already
376
+ // a `table.indexes[]` entry — it flows through `convertIndex` below, carrying
377
+ // the object→own-column `dependsOn` edge like any other index.
378
+ foreignKeys: table.foreignKeys.map((fk) => convertForeignKey(fk, storage)),
379
+ uniques: table.uniques.map((u) => convertUnique(u, name)),
380
+ indexes: table.indexes.map((i) => convertIndex(i, name)),
271
381
  ...ifDefined('checks', checks),
272
- };
382
+ });
273
383
  }
274
384
 
275
385
  /**
@@ -303,7 +413,7 @@ export function detectDestructiveChanges(
303
413
 
304
414
  for (const tableName of Object.keys(fromTables)) {
305
415
  const toTableRaw = toNs?.entries.table?.[tableName];
306
- if (!(toTableRaw instanceof StorageTable)) {
416
+ if (!StorageTable.is(toTableRaw)) {
307
417
  conflicts.push({
308
418
  kind: 'tableRemoved',
309
419
  summary: `Table "${tableName}" was removed`,
@@ -313,7 +423,7 @@ export function detectDestructiveChanges(
313
423
  const toTable = toTableRaw;
314
424
 
315
425
  const fromTableRaw = fromTables[tableName];
316
- if (!(fromTableRaw instanceof StorageTable)) continue;
426
+ if (!StorageTable.is(fromTableRaw)) continue;
317
427
  const fromTable = fromTableRaw;
318
428
 
319
429
  for (const columnName of Object.keys(fromTable.columns)) {
@@ -358,6 +468,48 @@ export interface ContractToSchemaIROptions {
358
468
  *
359
469
  * Returns an empty schema IR when `contract` is `null` (new project).
360
470
  */
471
+ /**
472
+ * Converts the tables of a single namespace into a `SqlSchemaIR`, keyed by
473
+ * table name within that namespace. Unlike {@link contractToSchemaIR}, which
474
+ * flattens every namespace's tables into one bare-keyed record (and throws on a
475
+ * cross-namespace name collision), this scopes the table iteration to one
476
+ * namespace so the same table name can exist in two schemas.
477
+ *
478
+ * The full `storage` is still passed to `convertTable`, so value-set / enum /
479
+ * type resolution that legitimately spans namespaces is unaffected. Foreign
480
+ * keys are built purely from the FK descriptor (`fk.target`), so cross-namespace
481
+ * FKs survive per-namespace conversion. The `annotations` block (storage-type
482
+ * derived) is omitted here — the per-namespace tree consumer reads only the
483
+ * per-table fields.
484
+ */
485
+ export function contractNamespaceToSchemaIR(
486
+ storage: SqlStorage,
487
+ namespaceId: string,
488
+ options: ContractToSchemaIROptions,
489
+ ): SqlSchemaIR {
490
+ if (options.annotationNamespace.length === 0) {
491
+ throw new Error('annotationNamespace must be a non-empty string');
492
+ }
493
+ const namespace = storage.namespaces[namespaceId];
494
+ if (!namespace) {
495
+ return new SqlSchemaIR({ tables: {} });
496
+ }
497
+ const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
498
+ const tables: Record<string, SqlTableIR> = {};
499
+ for (const [tableName, tableDefRaw] of Object.entries(namespace.entries.table ?? {})) {
500
+ StorageTable.assert(tableDefRaw, `namespaces.${namespaceId}.entries.table.${tableName}`);
501
+ tables[tableName] = convertTable(
502
+ tableName,
503
+ tableDefRaw,
504
+ storageTypes,
505
+ options.expandNativeType,
506
+ options.renderDefault,
507
+ storage,
508
+ );
509
+ }
510
+ return new SqlSchemaIR({ tables });
511
+ }
512
+
361
513
  export function contractToSchemaIR(
362
514
  contract: Contract<SqlStorage> | null,
363
515
  options: ContractToSchemaIROptions,
@@ -367,21 +519,15 @@ export function contractToSchemaIR(
367
519
  }
368
520
 
369
521
  if (!contract) {
370
- return { tables: {} };
522
+ return new SqlSchemaIR({ tables: {} });
371
523
  }
372
524
 
373
525
  const storage = contract.storage;
374
- const storageTypes: ResolvedStorageTypes = {
375
- ...((storage.types ?? {}) as ResolvedStorageTypes),
376
- };
526
+ const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
377
527
  const tables: Record<string, SqlTableIR> = {};
378
528
  for (const ns of Object.values(storage.namespaces)) {
379
529
  for (const [tableName, tableDefRaw] of Object.entries(ns.entries.table ?? {})) {
380
- if (!(tableDefRaw instanceof StorageTable)) {
381
- throw new Error(
382
- `contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.entries.table.${tableName}`,
383
- );
384
- }
530
+ StorageTable.assert(tableDefRaw, `namespaces.${ns.id}.entries.table.${tableName}`);
385
531
  const tableDef = tableDefRaw;
386
532
  if (tables[tableName] !== undefined) {
387
533
  throw new Error(
@@ -405,10 +551,10 @@ export function contractToSchemaIR(
405
551
  options.resolveEnumNamespaceSchema,
406
552
  );
407
553
 
408
- return {
554
+ return new SqlSchemaIR({
409
555
  tables,
410
556
  ...ifDefined('annotations', annotations),
411
- };
557
+ });
412
558
  }
413
559
 
414
560
  function deriveAnnotations(
@@ -418,7 +564,7 @@ function deriveAnnotations(
418
564
  ): SqlAnnotations | undefined {
419
565
  const storageTypes: Record<string, StorageTypeInstance> = {};
420
566
 
421
- for (const typeInstance of Object.values((storage.types ?? {}) as ResolvedStorageTypes)) {
567
+ for (const typeInstance of Object.values(storage.types ?? {})) {
422
568
  if (isStorageTypeInstance(typeInstance)) {
423
569
  storageTypes[typeInstance.nativeType] = typeInstance;
424
570
  }
@@ -5,20 +5,25 @@ import {
5
5
  } from '@prisma-next/contract/types';
6
6
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
7
7
  import { ifDefined } from '@prisma-next/utils/defined';
8
- import type { SqlPlannerConflict } from './types';
9
8
 
10
9
  /**
11
10
  * The target object a control policy governs for a single planner call,
12
11
  * resolved from the target's own IR. `undefined` means the call's target
13
12
  * object could not be positively established — a fail-closed signal: any
14
13
  * policy stricter than `managed` drops such a call rather than emitting it.
14
+ *
15
+ * The object identity is a generic storage coordinate — `(entityKind,
16
+ * entityName)`, absent for a whole-namespace subject — never a target-specific
17
+ * `table`/`typeName` field. The family reasons about the coordinate for policy
18
+ * and deduplication; the target owns any vocabulary (table vs enum) when it
19
+ * renders a suppression.
15
20
  */
16
21
  export interface ControlPolicySubject {
17
22
  readonly namespaceId: string;
18
- readonly explicitNodeControlPolicy?: ControlPolicy;
19
- readonly table?: string;
23
+ readonly entityKind?: string;
24
+ readonly entityName?: string;
20
25
  readonly column?: string;
21
- readonly typeName?: string;
26
+ readonly explicitNodeControlPolicy?: ControlPolicy;
22
27
  /**
23
28
  * Whether the call creates a whole, previously-absent top-level storage
24
29
  * object (e.g. a table or an enum/type), as opposed to modifying an
@@ -29,6 +34,26 @@ export interface ControlPolicySubject {
29
34
  readonly createsNewObject: boolean;
30
35
  }
31
36
 
37
+ /**
38
+ * The structured record a control-policy partition emits for one suppressed
39
+ * subject or call. It carries only raw data — no strings, no vocabulary — so
40
+ * the consumer (the target) renders the user-facing message itself:
41
+ *
42
+ * - `subject` — the suppressed subject (its coordinate + namespace), or
43
+ * `undefined` when the subject could not be resolved (fail-closed).
44
+ * - `policy` — the effective control policy that caused the suppression.
45
+ * - `factoryName` — the resolved creation factory name if the suppressed thing
46
+ * was a whole-object creation (e.g. `createTable`), else `undefined`. The
47
+ * family never invents a verb for a modification that produced no call.
48
+ * - `createsNewObject` — whether the suppressed thing was object creation.
49
+ */
50
+ export interface SuppressionRecord {
51
+ readonly subject: ControlPolicySubject | undefined;
52
+ readonly policy: ControlPolicy;
53
+ readonly factoryName: string | undefined;
54
+ readonly createsNewObject: boolean;
55
+ }
56
+
32
57
  /**
33
58
  * The control policy that governs a single call. The `external` default is an
34
59
  * un-overridable namespace floor: when the contract default is `external`, no
@@ -71,67 +96,10 @@ function callAllowedUnderControlPolicy(
71
96
  }
72
97
  }
73
98
 
74
- function defaultSubjectLabel(
75
- factoryName: string,
76
- subject: ControlPolicySubject | undefined,
77
- ): string {
78
- if (subject?.table) {
79
- return `${factoryName}(${subject.table})`;
80
- }
81
- if (subject?.typeName) {
82
- return `${factoryName}(${subject.typeName})`;
83
- }
84
- return factoryName;
85
- }
86
-
87
- function suppressionSummary(
88
- subjectLabel: string,
89
- subject: ControlPolicySubject | undefined,
90
- effectivePolicy: ControlPolicy,
91
- ): string {
92
- const namespace = subject?.namespaceId ?? 'unknown';
93
- const declared = subject?.explicitNodeControlPolicy;
94
- if (effectivePolicy === 'external' && declared === 'managed') {
95
- return `control policy suppressed: ${subjectLabel} — namespace '${namespace}' has effective control 'external' but table declared 'managed'`;
96
- }
97
- const declaredSuffix = declared ? ` but table declared '${declared}'` : '';
98
- return `control policy suppressed: ${subjectLabel} — namespace '${namespace}' has effective control '${effectivePolicy}'${declaredSuffix}`;
99
- }
100
-
101
- function buildSubjectSuppressionWarning(
102
- subject: ControlPolicySubject | undefined,
103
- effectivePolicy: ControlPolicy,
104
- factoryName: string,
105
- formatSubjectLabel: (factoryName: string, subject: ControlPolicySubject | undefined) => string,
106
- ): SqlPlannerConflict {
107
- const subjectLabel = formatSubjectLabel(factoryName, subject);
108
- return {
109
- kind: 'controlPolicySuppressedCall',
110
- summary: suppressionSummary(subjectLabel, subject, effectivePolicy),
111
- location: {
112
- ...ifDefined('namespace', subject?.namespaceId),
113
- ...ifDefined('table', subject?.table),
114
- ...ifDefined('column', subject?.column),
115
- ...ifDefined('type', subject?.typeName),
116
- },
117
- meta: {
118
- controlPolicy: effectivePolicy,
119
- factoryName,
120
- ...ifDefined('declaredControlPolicy', subject?.explicitNodeControlPolicy),
121
- },
122
- };
123
- }
124
-
125
- function defaultModificationFactoryNameForSubject(subject: ControlPolicySubject): string {
126
- if (subject.table) return 'alterTable';
127
- if (subject.typeName) return 'alterType';
128
- return 'alterSchema';
129
- }
130
-
131
99
  /**
132
100
  * Partition the calls produced for a single set of subjects into those the
133
101
  * effective control policy permits (`kept`) and a list of
134
- * {@link SqlPlannerConflict} warnings describing the suppressed calls.
102
+ * {@link SuppressionRecord}s describing the suppressed calls.
135
103
  *
136
104
  * **Prefer {@link partitionIssuesByControlPolicy}** for the schema-issue
137
105
  * pipeline: it filters subjects out of the planner's *input* so the planner
@@ -146,18 +114,13 @@ export function partitionCallsByControlPolicy<TCall>(options: {
146
114
  readonly contract: Contract<SqlStorage>;
147
115
  readonly resolveControlPolicySubject: (call: TCall) => ControlPolicySubject | undefined;
148
116
  readonly resolveFactoryName: (call: TCall) => string;
149
- readonly formatSubjectLabel?: (
150
- factoryName: string,
151
- subject: ControlPolicySubject | undefined,
152
- ) => string;
153
117
  }): {
154
118
  readonly kept: readonly TCall[];
155
- readonly warnings: readonly SqlPlannerConflict[];
119
+ readonly suppressions: readonly SuppressionRecord[];
156
120
  } {
157
121
  const defaultControlPolicy = options.contract.defaultControlPolicy;
158
- const formatSubjectLabel = options.formatSubjectLabel ?? defaultSubjectLabel;
159
122
  const kept: TCall[] = [];
160
- const warnings: SqlPlannerConflict[] = [];
123
+ const suppressions: SuppressionRecord[] = [];
161
124
 
162
125
  for (const call of options.calls) {
163
126
  const subject = options.resolveControlPolicySubject(call);
@@ -165,16 +128,18 @@ export function partitionCallsByControlPolicy<TCall>(options: {
165
128
  if (callAllowedUnderControlPolicy(policy, subject)) {
166
129
  kept.push(call);
167
130
  } else {
168
- const factoryName = options.resolveFactoryName(call);
169
- warnings.push(
170
- buildSubjectSuppressionWarning(subject, policy, factoryName, formatSubjectLabel),
171
- );
131
+ suppressions.push({
132
+ subject,
133
+ policy,
134
+ factoryName: options.resolveFactoryName(call),
135
+ createsNewObject: subject?.createsNewObject ?? false,
136
+ });
172
137
  }
173
138
  }
174
139
 
175
140
  return Object.freeze({
176
141
  kept: Object.freeze(kept),
177
- warnings: Object.freeze(warnings),
142
+ suppressions: Object.freeze(suppressions),
178
143
  });
179
144
  }
180
145
 
@@ -192,18 +157,15 @@ export function partitionCallsByControlPolicy<TCall>(options: {
192
157
  * cannot reason about the live shape of a subject the user marked as
193
158
  * out-of-scope (`external`).
194
159
  *
195
- * `warnings` is one {@link SqlPlannerConflict} per suppressed subject (not per
196
- * suppressed issue). `factoryName` is inferred from the subject's issue mix:
197
- * if any of the subject's issues is whole-object creation, the warning takes
198
- * the corresponding creation factoryName (e.g. `createTable`,
199
- * `createEnumType`, `createSchema`); otherwise it falls back to
200
- * `defaultModificationFactoryName(subject)` — a synthetic label that names
201
- * the *kind* of mutation that would have run, since no concrete DDL call was
202
- * generated.
160
+ * `suppressions` is one {@link SuppressionRecord} per suppressed subject (not
161
+ * per suppressed issue). Its `factoryName` is the creation factory name when any
162
+ * of the subject's issues is whole-object creation (e.g. `createTable`), else
163
+ * `undefined` — the family never invents a modification verb for an op that
164
+ * produced no call; the target renders the message.
203
165
  *
204
166
  * Unresolved-subject issues (`resolveControlPolicySubject` returns
205
- * `undefined`) emit one warning each; they cannot be deduplicated because
206
- * they carry no subject coordinate.
167
+ * `undefined`) emit one record each; they cannot be deduplicated because they
168
+ * carry no subject coordinate.
207
169
  */
208
170
  export function partitionIssuesByControlPolicy<TIssue>(options: {
209
171
  readonly issues: readonly TIssue[];
@@ -221,34 +183,19 @@ export function partitionIssuesByControlPolicy<TIssue>(options: {
221
183
  *
222
184
  * 1. Under `tolerated`, only issues whose `resolveCreationFactoryName`
223
185
  * returns a value flow into the planner (create-if-absent).
224
- * 2. Subjects that have at least one creation-flavoured issue use the
225
- * resolved creation factoryName for their suppression warning;
226
- * otherwise they fall back to `defaultModificationFactoryName`.
186
+ * 2. A suppressed subject's record carries the resolved creation factoryName
187
+ * when it has at least one creation-flavoured issue, else `undefined`.
227
188
  */
228
189
  readonly resolveCreationFactoryName: (issue: TIssue) => string | undefined;
229
- /**
230
- * Default modification factoryName for a suppressed subject whose issues
231
- * are all non-creation (the subject exists but has a different shape).
232
- * Defaults to `'alterTable'` / `'alterType'` / `'alterSchema'` based on the
233
- * subject's populated coordinates.
234
- */
235
- readonly defaultModificationFactoryName?: (subject: ControlPolicySubject) => string;
236
- readonly formatSubjectLabel?: (
237
- factoryName: string,
238
- subject: ControlPolicySubject | undefined,
239
- ) => string;
240
190
  }): {
241
191
  readonly plannable: readonly TIssue[];
242
- readonly warnings: readonly SqlPlannerConflict[];
192
+ readonly suppressions: readonly SuppressionRecord[];
243
193
  } {
244
194
  const defaultControlPolicy = options.contract.defaultControlPolicy;
245
- const formatSubjectLabel = options.formatSubjectLabel ?? defaultSubjectLabel;
246
- const inferModificationFactoryName =
247
- options.defaultModificationFactoryName ?? defaultModificationFactoryNameForSubject;
248
195
 
249
196
  const plannable: TIssue[] = [];
250
197
  // Resolved-subject suppressions are deduplicated by subject key so we emit
251
- // one warning per suppressed subject, not one per suppressed issue.
198
+ // one record per suppressed subject, not one per suppressed issue.
252
199
  // `creationFactoryName` upgrades from `undefined` to a concrete creation
253
200
  // name the first time we see a creation-flavoured issue for the subject.
254
201
  const suppressedSubjects = new Map<
@@ -259,7 +206,7 @@ export function partitionIssuesByControlPolicy<TIssue>(options: {
259
206
  creationFactoryName?: string;
260
207
  }
261
208
  >();
262
- const unresolvedSuppressions: SqlPlannerConflict[] = [];
209
+ const unresolvedSuppressions: SuppressionRecord[] = [];
263
210
 
264
211
  for (const issue of options.issues) {
265
212
  const subject = options.resolveControlPolicySubject(issue);
@@ -281,10 +228,12 @@ export function partitionIssuesByControlPolicy<TIssue>(options: {
281
228
  }
282
229
 
283
230
  if (subject === undefined) {
284
- const factoryName = creationFactoryName ?? 'unknown';
285
- unresolvedSuppressions.push(
286
- buildSubjectSuppressionWarning(undefined, policy, factoryName, formatSubjectLabel),
287
- );
231
+ unresolvedSuppressions.push({
232
+ subject: undefined,
233
+ policy,
234
+ factoryName: creationFactoryName,
235
+ createsNewObject: false,
236
+ });
288
237
  continue;
289
238
  }
290
239
 
@@ -303,20 +252,22 @@ export function partitionIssuesByControlPolicy<TIssue>(options: {
303
252
  }
304
253
  }
305
254
 
306
- const warnings: SqlPlannerConflict[] = [...unresolvedSuppressions];
255
+ const suppressions: SuppressionRecord[] = [...unresolvedSuppressions];
307
256
  for (const entry of suppressedSubjects.values()) {
308
- const factoryName = entry.creationFactoryName ?? inferModificationFactoryName(entry.subject);
309
- warnings.push(
310
- buildSubjectSuppressionWarning(entry.subject, entry.policy, factoryName, formatSubjectLabel),
311
- );
257
+ suppressions.push({
258
+ subject: entry.subject,
259
+ policy: entry.policy,
260
+ factoryName: entry.creationFactoryName,
261
+ createsNewObject: entry.subject.createsNewObject,
262
+ });
312
263
  }
313
264
 
314
265
  return Object.freeze({
315
266
  plannable: Object.freeze(plannable),
316
- warnings: Object.freeze(warnings),
267
+ suppressions: Object.freeze(suppressions),
317
268
  });
318
269
  }
319
270
 
320
271
  function subjectKey(subject: ControlPolicySubject): string {
321
- return `${subject.namespaceId}\u0000${subject.table ?? ''}\u0000${subject.typeName ?? ''}`;
272
+ return `${subject.namespaceId}\u0000${subject.entityKind ?? ''}\u0000${subject.entityName ?? ''}`;
322
273
  }