@prisma-next/family-sql 0.15.0-dev.2 → 0.15.0-dev.20

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.
@@ -1,6 +1,9 @@
1
1
  import type { ColumnDefault, Contract, JsonValue } from '@prisma-next/contract/types';
2
2
  import type { CodecRef } from '@prisma-next/framework-components/codec';
3
- import type { MigrationPlannerConflict } from '@prisma-next/framework-components/control';
3
+ import type {
4
+ MigrationPlannerConflict,
5
+ SchemaNodeRef,
6
+ } from '@prisma-next/framework-components/control';
4
7
  import {
5
8
  type CheckConstraint,
6
9
  type ForeignKey,
@@ -13,6 +16,7 @@ import {
13
16
  type UniqueConstraint,
14
17
  } from '@prisma-next/sql-contract/types';
15
18
  import {
19
+ RelationalSchemaNodeKind,
16
20
  type SqlAnnotations,
17
21
  type SqlCheckConstraintIRInput,
18
22
  type SqlColumnIRInput,
@@ -52,6 +56,17 @@ export type NativeTypeExpander = (input: {
52
56
  */
53
57
  export type DefaultRenderer = (def: ColumnDefault, column: StorageColumn) => string;
54
58
 
59
+ /**
60
+ * Target-supplied hook (same IoC seam as `NativeTypeExpander`/`DefaultRenderer`)
61
+ * that normalizes a contract-declared `ColumnDefault` into the resolved shape
62
+ * the target's introspection parses from the live database — e.g. a
63
+ * `dbgenerated("'{}'::jsonb")` function call and the literal Postgres reports
64
+ * are the same value in different shapes, and `resolvedDefaultsEqual`
65
+ * compares `kind` before content. When omitted, the contract's raw default
66
+ * is the resolved default unchanged.
67
+ */
68
+ export type DefaultResolver = (def: ColumnDefault, resolvedNativeType: string) => ColumnDefault;
69
+
55
70
  /**
56
71
  * Target-supplied callback that resolves a contract namespace to the live
57
72
  * database schema its enums are stored under.
@@ -73,6 +88,7 @@ function convertColumn(
73
88
  storageTypes: ResolvedStorageTypes,
74
89
  expandNativeType: NativeTypeExpander | undefined,
75
90
  renderDefault: DefaultRenderer | undefined,
91
+ resolveDefault: DefaultResolver | undefined,
76
92
  ): SqlColumnIRInput {
77
93
  // Resolve `typeRef` so columns that delegate their `nativeType`/`codecId`/
78
94
  // `typeParams` to a named `storage.types` entry expand the same way as
@@ -103,6 +119,11 @@ function convertColumn(
103
119
  // agree on as the comparable "expanded" type.
104
120
  const nativeType = baseNativeType;
105
121
  const resolvedNativeType = column.many ? `${baseNativeType}[]` : baseNativeType;
122
+ const rawColumnDefault = column.default ?? undefined;
123
+ const resolvedColumnDefault =
124
+ rawColumnDefault !== undefined && resolveDefault
125
+ ? resolveDefault(rawColumnDefault, resolvedNativeType)
126
+ : rawColumnDefault;
106
127
  return {
107
128
  name,
108
129
  nativeType,
@@ -113,11 +134,14 @@ function convertColumn(
113
134
  column.default != null && renderDefault ? renderDefault(column.default, column) : undefined,
114
135
  ),
115
136
  // Contract-derived columns are resolved by construction: the computed
116
- // full native type doubles as the resolved value, and the contract's
117
- // structured default is the resolved default (the introspected side
118
- // stamps its normalizer's parse of the raw expression).
137
+ // full native type doubles as the resolved value. The contract's raw
138
+ // structured default becomes the resolved default after passing through
139
+ // the target's `resolveDefault` hook (when supplied), so a default the
140
+ // target's introspection side would normalize differently (e.g. a
141
+ // `dbgenerated(...)` function call that is actually a literal) compares
142
+ // equal instead of drifting on `kind` alone.
119
143
  resolvedNativeType,
120
- ...ifDefined('resolvedDefault', column.default ?? undefined),
144
+ ...ifDefined('resolvedDefault', resolvedColumnDefault),
121
145
  // The column's codec identity, carried the same way the query AST
122
146
  // carries `CodecRef` (TML-2456) — the migration planner's op-builders
123
147
  // resolve DDL rendering from this at plan time (Decision 5), instead of
@@ -244,14 +268,15 @@ function convertCheck(check: CheckConstraint, storage: SqlStorage): SqlCheckCons
244
268
  };
245
269
  }
246
270
 
247
- function convertUnique(unique: UniqueConstraint): SqlUniqueIRInput {
271
+ function convertUnique(unique: UniqueConstraint, tableName: string): SqlUniqueIRInput {
248
272
  return {
249
273
  columns: unique.columns,
250
274
  ...ifDefined('name', unique.name),
275
+ dependsOn: flatColumnDependsOn(tableName, unique.columns),
251
276
  };
252
277
  }
253
278
 
254
- function convertIndex(index: Index): SqlIndexIRInput {
279
+ function convertIndex(index: Index, tableName: string): SqlIndexIRInput {
255
280
  return {
256
281
  columns: index.columns,
257
282
  unique: false,
@@ -260,9 +285,42 @@ function convertIndex(index: Index): SqlIndexIRInput {
260
285
  // introspected side (the legacy walk read them from the contract).
261
286
  ...ifDefined('type', index.type),
262
287
  ...ifDefined('options', index.options),
288
+ dependsOn: flatColumnDependsOn(tableName, index.columns),
263
289
  };
264
290
  }
265
291
 
292
+ /**
293
+ * The referenced table's chain in the flat (single-schema) tree
294
+ * `contractToSchemaIR`/`contractNamespaceToSchemaIR` build: the root
295
+ * (`SqlSchemaIR`, fixed `'database'` id) followed by the table's own id.
296
+ * Postgres discards this when it re-derives the FK against its own
297
+ * multi-schema tree shape (`contractToPostgresDatabaseSchemaNode`); SQLite's
298
+ * flat tree uses it as-is.
299
+ */
300
+ function flatSchemaDependsOn(tableName: string): SchemaNodeRef {
301
+ return [
302
+ { nodeKind: RelationalSchemaNodeKind.schema, id: 'database' },
303
+ { nodeKind: RelationalSchemaNodeKind.table, id: tableName },
304
+ ];
305
+ }
306
+
307
+ /**
308
+ * The chains from a table-child object (foreign key, index, unique, primary
309
+ * key) to each of the own columns it is built on, in the flat tree. Dropping
310
+ * a covered column auto-drops the object, so the object's drop must precede
311
+ * the column's; the graph derives that direction from these edges.
312
+ */
313
+ function flatColumnDependsOn(
314
+ tableName: string,
315
+ columns: readonly string[],
316
+ ): readonly SchemaNodeRef[] {
317
+ return columns.map((column) => [
318
+ { nodeKind: RelationalSchemaNodeKind.schema, id: 'database' },
319
+ { nodeKind: RelationalSchemaNodeKind.table, id: tableName },
320
+ { nodeKind: RelationalSchemaNodeKind.column, id: `column:${column}` },
321
+ ]);
322
+ }
323
+
266
324
  /**
267
325
  * The FK's referenced-namespace identity comes from the target's namespace
268
326
  * node, not the raw namespace-id string. An unbound target namespace stamps
@@ -272,6 +330,10 @@ function convertIndex(index: Index): SqlIndexIRInput {
272
330
  * cross-space target whose namespace lives in another contract's storage)
273
331
  * stamps its coordinate verbatim; namespaced targets (Postgres) resolve the
274
332
  * real DDL schema downstream.
333
+ *
334
+ * `dependsOn` carries the referenced table (created before the FK, dropped
335
+ * after it) plus the FK's own columns (dropped after the FK, since dropping a
336
+ * column auto-drops the FK built on it).
275
337
  */
276
338
  function convertForeignKey(fk: ForeignKey, storage: SqlStorage): SqlForeignKeyIRInput {
277
339
  const targetNamespace = storage.namespaces[fk.target.namespaceId];
@@ -284,6 +346,10 @@ function convertForeignKey(fk: ForeignKey, storage: SqlStorage): SqlForeignKeyIR
284
346
  ...ifDefined('name', fk.name),
285
347
  ...ifDefined('onDelete', fk.onDelete),
286
348
  ...ifDefined('onUpdate', fk.onUpdate),
349
+ dependsOn: [
350
+ flatSchemaDependsOn(fk.target.tableName),
351
+ ...flatColumnDependsOn(fk.source.tableName, fk.source.columns),
352
+ ],
287
353
  };
288
354
  }
289
355
 
@@ -293,6 +359,7 @@ function convertTable(
293
359
  storageTypes: ResolvedStorageTypes,
294
360
  expandNativeType: NativeTypeExpander | undefined,
295
361
  renderDefault: DefaultRenderer | undefined,
362
+ resolveDefault: DefaultResolver | undefined,
296
363
  storage: SqlStorage,
297
364
  ): SqlTableIR {
298
365
  const columns: Record<string, SqlColumnIRInput> = {};
@@ -303,6 +370,7 @@ function convertTable(
303
370
  storageTypes,
304
371
  expandNativeType,
305
372
  renderDefault,
373
+ resolveDefault,
306
374
  );
307
375
  }
308
376
 
@@ -311,13 +379,27 @@ function convertTable(
311
379
  ? table.checks.map((c) => convertCheck(c, storage))
312
380
  : undefined;
313
381
 
382
+ const primaryKey =
383
+ table.primaryKey !== undefined
384
+ ? {
385
+ columns: table.primaryKey.columns,
386
+ ...ifDefined('name', table.primaryKey.name),
387
+ dependsOn: flatColumnDependsOn(name, table.primaryKey.columns),
388
+ }
389
+ : undefined;
390
+
314
391
  return new SqlTableIR({
315
392
  name,
316
393
  columns,
317
- ...ifDefined('primaryKey', table.primaryKey),
394
+ ...ifDefined('primaryKey', primaryKey),
395
+ // #989 persists a `constraint: false` FK's absence and its backing index as
396
+ // discrete entities at contract construction, so every `foreignKeys[]` entry
397
+ // is now constraint-bearing (no filter) and each FK-backing index is already
398
+ // a `table.indexes[]` entry — it flows through `convertIndex` below, carrying
399
+ // the object→own-column `dependsOn` edge like any other index.
318
400
  foreignKeys: table.foreignKeys.map((fk) => convertForeignKey(fk, storage)),
319
- uniques: table.uniques.map(convertUnique),
320
- indexes: table.indexes.map(convertIndex),
401
+ uniques: table.uniques.map((u) => convertUnique(u, name)),
402
+ indexes: table.indexes.map((i) => convertIndex(i, name)),
321
403
  ...ifDefined('checks', checks),
322
404
  });
323
405
  }
@@ -384,6 +466,7 @@ export interface ContractToSchemaIROptions {
384
466
  readonly annotationNamespace: string;
385
467
  readonly expandNativeType?: NativeTypeExpander;
386
468
  readonly renderDefault?: DefaultRenderer;
469
+ readonly resolveDefault?: DefaultResolver;
387
470
  /**
388
471
  * Target-supplied resolver mapping a namespace to the live database schema
389
472
  * its enums are stored under. When provided (Postgres), namespace-scoped
@@ -444,6 +527,7 @@ export function contractNamespaceToSchemaIR(
444
527
  storageTypes,
445
528
  options.expandNativeType,
446
529
  options.renderDefault,
530
+ options.resolveDefault,
447
531
  storage,
448
532
  );
449
533
  }
@@ -480,6 +564,7 @@ export function contractToSchemaIR(
480
564
  storageTypes,
481
565
  options.expandNativeType,
482
566
  options.renderDefault,
567
+ options.resolveDefault,
483
568
  storage,
484
569
  );
485
570
  }
@@ -1,3 +1,5 @@
1
+ import pluralizeLib from 'pluralize';
2
+
1
3
  const PSL_RESERVED_WORDS = new Set(['model', 'enum', 'types', 'type', 'generator', 'datasource']);
2
4
 
3
5
  const IDENTIFIER_PART_PATTERN = /[A-Za-z0-9]+/g;
@@ -143,19 +145,7 @@ export function toEnumMemberName(value: string): string {
143
145
  }
144
146
 
145
147
  export function pluralize(word: string): string {
146
- if (
147
- word.endsWith('s') ||
148
- word.endsWith('x') ||
149
- word.endsWith('z') ||
150
- word.endsWith('ch') ||
151
- word.endsWith('sh')
152
- ) {
153
- return `${word}es`;
154
- }
155
- if (word.endsWith('y') && !/[aeiou]y$/i.test(word)) {
156
- return `${word.slice(0, -1)}ies`;
157
- }
158
- return `${word}s`;
148
+ return pluralizeLib.plural(word);
159
149
  }
160
150
 
161
151
  export function deriveRelationFieldName(
@@ -73,3 +73,91 @@ export function temporalAuthoringPresets<
73
73
  },
74
74
  } as const satisfies Record<string, AuthoringFieldPresetDescriptor>;
75
75
  }
76
+
77
+ const TEMPORAL_PRECISION_ARG = {
78
+ name: 'precision',
79
+ kind: 'number',
80
+ optional: true,
81
+ integer: true,
82
+ minimum: 0,
83
+ } as const;
84
+
85
+ const TEMPORAL_ON_CREATE_ARG = {
86
+ name: 'onCreate',
87
+ kind: 'option',
88
+ values: ['now'],
89
+ optional: true,
90
+ } as const;
91
+
92
+ const TEMPORAL_ON_UPDATE_ARG = {
93
+ name: 'onUpdate',
94
+ kind: 'option',
95
+ values: ['now'],
96
+ optional: true,
97
+ } as const;
98
+
99
+ /**
100
+ * Selects the `timestampNow` generator descriptor for the preset's `now`
101
+ * token. The token is preset vocabulary; the generator id never appears in a
102
+ * user's spelling (ADR 169 — `timestampNow` is preset-only).
103
+ */
104
+ function temporalPhaseTemplate<const Index extends number>(index: Index) {
105
+ return {
106
+ kind: 'select',
107
+ index,
108
+ cases: { now: { kind: 'generator', id: TIMESTAMP_NOW_GENERATOR_ID } },
109
+ } as const;
110
+ }
111
+
112
+ /**
113
+ * Builds a `temporal.<codec>` field preset for a codec that takes a precision
114
+ * parameter (`pg/timestamp@1`, `pg/timestamptz@1`). Arguments change field
115
+ * properties only — never the codec, which the caller fixes here.
116
+ *
117
+ * All three arguments are optional: omitting `precision` omits `typeParams`
118
+ * entirely, and omitting a phase omits that phase (both omitted omits
119
+ * `executionDefaults`).
120
+ */
121
+ /* @__NO_SIDE_EFFECTS__ */
122
+ export function temporalCodecPresetWithPrecision<
123
+ const CodecId extends string,
124
+ const NativeType extends string,
125
+ >(input: { readonly codecId: CodecId; readonly nativeType: NativeType }) {
126
+ return {
127
+ kind: 'fieldPreset',
128
+ args: [TEMPORAL_PRECISION_ARG, TEMPORAL_ON_CREATE_ARG, TEMPORAL_ON_UPDATE_ARG],
129
+ output: {
130
+ codecId: input.codecId,
131
+ nativeType: input.nativeType,
132
+ typeParams: { precision: { kind: 'arg', index: 0 } },
133
+ executionDefaults: {
134
+ onCreate: temporalPhaseTemplate(1),
135
+ onUpdate: temporalPhaseTemplate(2),
136
+ },
137
+ },
138
+ } as const satisfies AuthoringFieldPresetDescriptor;
139
+ }
140
+
141
+ /**
142
+ * Builds a `temporal.<codec>` field preset for a codec with no type
143
+ * parameters (`sqlite/datetime@1`). As with the precision-bearing variant,
144
+ * both phase arguments are optional and omitting one omits that phase.
145
+ */
146
+ /* @__NO_SIDE_EFFECTS__ */
147
+ export function temporalCodecPreset<
148
+ const CodecId extends string,
149
+ const NativeType extends string,
150
+ >(input: { readonly codecId: CodecId; readonly nativeType: NativeType }) {
151
+ return {
152
+ kind: 'fieldPreset',
153
+ args: [TEMPORAL_ON_CREATE_ARG, TEMPORAL_ON_UPDATE_ARG],
154
+ output: {
155
+ codecId: input.codecId,
156
+ nativeType: input.nativeType,
157
+ executionDefaults: {
158
+ onCreate: temporalPhaseTemplate(0),
159
+ onUpdate: temporalPhaseTemplate(1),
160
+ },
161
+ },
162
+ } as const satisfies AuthoringFieldPresetDescriptor;
163
+ }
@@ -21,6 +21,7 @@ export type {
21
21
  export type {
22
22
  ContractToSchemaIROptions,
23
23
  DefaultRenderer,
24
+ DefaultResolver,
24
25
  EnumNamespaceSchemaResolver,
25
26
  NativeTypeExpander,
26
27
  } from '../core/migrations/contract-to-schema-ir';
@@ -87,6 +88,8 @@ export type {
87
88
  } from '../core/migrations/types';
88
89
  export {
89
90
  temporalAuthoringPresets,
91
+ temporalCodecPreset,
92
+ temporalCodecPresetWithPrecision,
90
93
  timestampNowControlDescriptor,
91
94
  } from '../core/timestamp-now-generator';
92
95
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"schema-verify-W3r631Jh.mjs","names":["d"],"sources":["../src/core/assembly.ts","../src/core/diff/verifier-disposition.ts","../src/core/diff/schema-verify.ts"],"sourcesContent":["import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';\nimport { assertUniqueCodecOwner } from '@prisma-next/framework-components/control';\nimport type { CodecControlHooks } from './migrations/types';\n\ntype CodecControlHooksMap = Record<string, CodecControlHooks>;\n\nfunction hasCodecControlHooks(descriptor: unknown): descriptor is {\n readonly id: string;\n readonly types: {\n readonly codecTypes: {\n readonly controlPlaneHooks: CodecControlHooksMap;\n };\n };\n} {\n if (typeof descriptor !== 'object' || descriptor === null) {\n return false;\n }\n const d = descriptor as { types?: { codecTypes?: { controlPlaneHooks?: unknown } } };\n const hooks = d.types?.codecTypes?.controlPlaneHooks;\n return hooks !== null && hooks !== undefined && typeof hooks === 'object';\n}\n\nexport function extractCodecControlHooks(\n descriptors: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>,\n): Map<string, CodecControlHooks> {\n const hooks = new Map<string, CodecControlHooks>();\n const owners = new Map<string, string>();\n\n for (const descriptor of descriptors) {\n if (typeof descriptor !== 'object' || descriptor === null) {\n continue;\n }\n if (!hasCodecControlHooks(descriptor)) {\n continue;\n }\n const controlPlaneHooks = descriptor.types.codecTypes.controlPlaneHooks;\n for (const [codecId, hook] of Object.entries(controlPlaneHooks)) {\n assertUniqueCodecOwner({\n codecId,\n owners,\n descriptorId: descriptor.id,\n entityLabel: 'control hooks',\n entityOwnershipLabel: 'owner',\n });\n hooks.set(codecId, hook);\n owners.set(codecId, descriptor.id);\n }\n }\n\n return hooks;\n}\n","import type { ControlPolicy } from '@prisma-next/contract/types';\nimport type {\n SchemaDiffIssue,\n VerifierIssueCategory,\n VerifierOutcome,\n} from '@prisma-next/framework-components/control';\nimport { dispositionForCategory } from '@prisma-next/framework-components/control';\n\n/**\n * Classifies a codec `verifyType` hook finding into the target-neutral\n * categories the framework grades. A storage type is a named type instance\n * (e.g. a native enum); the only shape divergence it can carry is a change\n * to its value set, so a paired mismatch always classifies as `valueDrift`.\n */\nexport function classifyStorageTypeDiffIssue(issue: SchemaDiffIssue): VerifierIssueCategory {\n if (issue.reason === 'not-found') {\n return 'declaredMissing';\n }\n if (issue.reason === 'not-expected') {\n return 'extraAuxiliary';\n }\n return 'valueDrift';\n}\n\nexport function verifierDisposition(\n controlPolicy: ControlPolicy,\n issue: SchemaDiffIssue,\n): VerifierOutcome {\n return dispositionForCategory(controlPolicy, classifyStorageTypeDiffIssue(issue));\n}\n","/**\n * The differ-based SQL schema verify: post-diff filters and verdict.\n *\n * The generic node differ (`diffSchemas`) reports every node-level\n * difference between the derived expected tree and the introspected actual\n * tree. This module is the consumer side the spec assigns to the SQL\n * family: strict-mode extras gating and control-policy disposition are\n * reason/kind-keyed filters applied AFTER the diff — never inside it — and\n * the verify verdict derives from the filtered issue list.\n *\n * `verifySqlSchemaByDiff` wraps the verdict in the issue-based result\n * envelope — this is THE SQL schema verify (the legacy relational walk and\n * its verification tree are retired).\n */\n\nimport type { Contract, ControlPolicy } from '@prisma-next/contract/types';\nimport { effectiveControlPolicy } from '@prisma-next/contract/types';\nimport type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';\nimport type {\n DiffSubjectGranularity,\n SchemaDiffIssue,\n VerifierIssueCategory,\n VerifierOutcome,\n VerifyDatabaseSchemaResult,\n} from '@prisma-next/framework-components/control';\nimport { dispositionForCategory } from '@prisma-next/framework-components/control';\nimport { isStorageTypeInstance, type SqlStorage } from '@prisma-next/sql-contract/types';\nimport { RelationalSchemaNodeKind, type SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';\nimport { blindCast } from '@prisma-next/utils/casts';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { extractCodecControlHooks } from '../assembly';\nimport type { SqlSchemaDiffFn } from '../migrations/schema-differ';\nimport type { CodecControlHooks } from '../migrations/types';\nimport { verifierDisposition } from './verifier-disposition';\n\n// ============================================================================\n// Subject-granularity classification — nodeKind → framework-neutral granularity\n// ============================================================================\n\nfunction issueNode(issue: SchemaDiffIssue): SqlSchemaIRNode | undefined {\n const node = issue.expected ?? issue.actual;\n if (node === undefined) return undefined;\n return blindCast<\n SqlSchemaIRNode,\n 'every node in a SQL schema diff tree is a SqlSchemaIRNode; nodeKind is its identity'\n >(node);\n}\n\n/**\n * Resolves an issue's framework-neutral {@link DiffSubjectGranularity} on\n * demand, from the issue's node's `nodeKind` via the target-provided\n * `granularityOf` map. The node carries only its `nodeKind` identity, never a\n * classification, and nothing is stamped onto the issue — every consumer\n * (the family verdict below, the framework aggregate's unclaimed-elements\n * sweep via {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable})\n * calls this the same way, resolved by the family/target that owns the node\n * vocabulary. `undefined` for an issue with no node.\n */\nexport function classifyDiffSubjectGranularity(\n issue: SchemaDiffIssue,\n granularityOf: (nodeKind: string) => DiffSubjectGranularity,\n): DiffSubjectGranularity | undefined {\n const node = issueNode(issue);\n return node === undefined ? undefined : granularityOf(node.nodeKind);\n}\n\n/**\n * Resolves an issue's storage `entityKind` on demand, from the issue's\n * node's `nodeKind` via the target-provided `entityKindOf` map — the sibling\n * of {@link classifyDiffSubjectGranularity}, called the same way by the same\n * consumers (via\n * {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}).\n * `undefined` for an issue with no node, or for a node kind with no storage\n * entity of its own.\n */\nexport function classifyDiffEntityKind(\n issue: SchemaDiffIssue,\n entityKindOf: (nodeKind: string) => string | undefined,\n): string | undefined {\n const node = issueNode(issue);\n return node === undefined ? undefined : entityKindOf(node.nodeKind);\n}\n\n// ============================================================================\n// Issue classification — subject granularity + reason → target-neutral category\n// ============================================================================\n\n/**\n * Re-keys the legacy `classifySqlVerifierIssueKind` category mapping on the\n * issue's {@link DiffSubjectGranularity} (resolved via `granularityOf`) + the\n * issue reason. The vocabulary maps one-to-one: an undeclared live entity or\n * namespace is `extraTopLevelObject`, an undeclared live field\n * `extraNestedElement`, undeclared auxiliaries (constraints, indexes,\n * defaults) and structural leaves (policies) `extraAuxiliary`; a value-set\n * drift on a check node is `valueDrift`; every other paired divergence is\n * `declaredIncompatible`; anything the database lacks is `declaredMissing`.\n * `granularityOf` is the target's classifier, so target and extension node\n * kinds classify without the family importing them.\n */\nexport function classifySqlDiffIssue(\n issue: SchemaDiffIssue,\n granularityOf: (nodeKind: string) => DiffSubjectGranularity,\n): VerifierIssueCategory {\n if (issue.reason === 'not-found') {\n return 'declaredMissing';\n }\n if (issue.reason === 'not-expected') {\n const granularity = classifyDiffSubjectGranularity(issue, granularityOf);\n if (granularity === 'entity' || granularity === 'namespace') {\n return 'extraTopLevelObject';\n }\n if (granularity === 'field') {\n return 'extraNestedElement';\n }\n return 'extraAuxiliary';\n }\n if (issueNode(issue)?.nodeKind === RelationalSchemaNodeKind.check) {\n return 'valueDrift';\n }\n return 'declaredIncompatible';\n}\n\n/**\n * Whether a `not-expected` issue is a strict-mode-only finding. The legacy\n * walk detected every relational extra (namespaces, entities, fields, and\n * their auxiliaries) only under `--strict`; the structural diff (roots, RLS\n * policies, roles) was never strict-gated — its extras fail in both modes.\n * Keyed on the issue's granularity, resolved via `granularityOf`.\n */\nfunction isStrictOnlyExtra(\n issue: SchemaDiffIssue,\n granularityOf: (nodeKind: string) => DiffSubjectGranularity,\n): boolean {\n const granularity = classifyDiffSubjectGranularity(issue, granularityOf);\n return (\n granularity === 'namespace' ||\n granularity === 'entity' ||\n granularity === 'field' ||\n granularity === 'auxiliary'\n );\n}\n\n// ============================================================================\n// The post-diff filter + verdict\n// ============================================================================\n\nexport interface SqlDiffVerdictInput {\n /** The full, ownership-scoped diff issue list from the target's differ. */\n readonly issues: readonly SchemaDiffIssue[];\n /** Resolves a diff issue's subject table's declared control policy directly from the contract. */\n readonly resolveControlPolicy: (issue: SchemaDiffIssue) => ControlPolicy | undefined;\n readonly strict: boolean;\n readonly defaultControlPolicy: ControlPolicy | undefined;\n /** The target's classifier: a diff issue node's `nodeKind` → its subject granularity. */\n readonly granularityOf: (nodeKind: string) => DiffSubjectGranularity;\n}\n\nexport interface SqlDiffVerdict {\n readonly failures: readonly SchemaDiffIssue[];\n readonly warnings: readonly SchemaDiffIssue[];\n}\n\n/**\n * Applies the two consumer filters to a diff issue list: strict gating\n * (relational `not-expected` findings drop in lenient mode) and\n * control-policy disposition (each surviving issue grades against its\n * subject table's effective policy; suppressed issues drop, `observed`\n * subjects warn). The verify verdict is `failures.length === 0`.\n */\nexport function computeSqlDiffVerdict(input: SqlDiffVerdictInput): SqlDiffVerdict {\n const failures: SchemaDiffIssue[] = [];\n const warnings: SchemaDiffIssue[] = [];\n for (const issue of input.issues) {\n if (\n !input.strict &&\n issue.reason === 'not-expected' &&\n isStrictOnlyExtra(issue, input.granularityOf)\n ) {\n continue;\n }\n const tablePolicy = input.resolveControlPolicy(issue);\n const policy = effectiveControlPolicy(tablePolicy, input.defaultControlPolicy);\n const disposition: VerifierOutcome = dispositionForCategory(\n policy,\n classifySqlDiffIssue(issue, input.granularityOf),\n );\n if (disposition === 'suppress') continue;\n if (disposition === 'warn') {\n warnings.push(issue);\n continue;\n }\n failures.push(issue);\n }\n return { failures, warnings };\n}\n\n// ============================================================================\n// Storage-types check — the codec verifyType hook path\n// ============================================================================\n\nexport interface StorageTypeVerdictInput {\n readonly contract: Contract<SqlStorage>;\n /**\n * Expected/actual namespace-node pairs the target's differ input produced:\n * for a namespaced tree, one entry per expected namespace with a\n * non-empty table set, paired by DDL schema name (absent actual side for\n * a schema the database lacks); a flat tree is the sole pair.\n */\n readonly namespacePairs: ReadonlyArray<{\n readonly actual: SqlSchemaIRNode | undefined;\n }>;\n readonly codecHooks: ReadonlyMap<string, CodecControlHooks>;\n}\n\n/**\n * Runs the codec `verifyType` hooks the way the legacy walk did: once per\n * contract namespace with tables, against that namespace's paired actual\n * node (the hook reads namespace-scoped state such as `enums` off it).\n * Issue dispositions grade against the contract default policy, matching\n * the legacy `pushTypeNode` semantics.\n */\nexport interface StorageTypeVerdict {\n readonly failures: readonly SchemaDiffIssue[];\n readonly warnings: readonly SchemaDiffIssue[];\n}\n\nexport function computeStorageTypeVerdict(input: StorageTypeVerdictInput): StorageTypeVerdict {\n const failures: SchemaDiffIssue[] = [];\n const warnings: SchemaDiffIssue[] = [];\n const policy = effectiveControlPolicy(undefined, input.contract.defaultControlPolicy);\n for (const pair of input.namespacePairs) {\n if (pair.actual === undefined) continue;\n for (const [typeName, typeInstance] of Object.entries(input.contract.storage.types ?? {})) {\n if (!isStorageTypeInstance(typeInstance)) continue;\n const hook = input.codecHooks.get(typeInstance.codecId);\n if (!hook?.verifyType) continue;\n const typeIssues = hook.verifyType({ typeName, typeInstance, schema: pair.actual });\n for (const issue of typeIssues) {\n const disposition = verifierDisposition(policy, issue);\n if (disposition === 'suppress') continue;\n if (disposition === 'warn') {\n warnings.push(issue);\n continue;\n }\n failures.push(issue);\n }\n }\n }\n return { failures, warnings };\n}\n\n// ============================================================================\n// The issue-based verify envelope\n// ============================================================================\n\nexport interface VerifySqlSchemaByDiffInput {\n readonly contract: Contract<SqlStorage>;\n readonly schema: SqlSchemaIRNode;\n readonly strict: boolean;\n readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;\n /** The target's full-tree node diff (`diffSchema` descriptor hook). */\n readonly diffSchema: SqlSchemaDiffFn;\n /** The target's classifier: a diff issue node's `nodeKind` → its subject granularity. */\n readonly granularityOf: (nodeKind: string) => DiffSubjectGranularity;\n}\n\n/**\n * THE SQL schema verify: runs the target's full-tree node diff, grades it\n * through the family's post-diff filters (strict gating + control-policy\n * disposition) plus the codec `verifyType` hook findings, and wraps the\n * verdict in the issue-based result envelope. `ok` holds exactly when both\n * issue lists are empty — the lists carry the verdict's failures.\n */\nexport function verifySqlSchemaByDiff(\n input: VerifySqlSchemaByDiffInput,\n): VerifyDatabaseSchemaResult {\n const startTime = Date.now();\n const verdictDiff = input.diffSchema({\n contract: input.contract,\n schema: input.schema,\n frameworkComponents: input.frameworkComponents,\n });\n const diffVerdict = computeSqlDiffVerdict({\n issues: verdictDiff.issues,\n resolveControlPolicy: verdictDiff.resolveControlPolicy,\n strict: input.strict,\n defaultControlPolicy: input.contract.defaultControlPolicy,\n granularityOf: input.granularityOf,\n });\n const storageTypeVerdict = computeStorageTypeVerdict({\n contract: input.contract,\n namespacePairs: verdictDiff.namespacePairs,\n codecHooks: extractCodecControlHooks(input.frameworkComponents),\n });\n const failCount = diffVerdict.failures.length + storageTypeVerdict.failures.length;\n const ok = failCount === 0;\n const profileHash =\n 'profileHash' in input.contract && typeof input.contract.profileHash === 'string'\n ? input.contract.profileHash\n : undefined;\n return {\n ok,\n ...(ok ? {} : { code: 'PN-SCHEMA-0001' }),\n summary: ok\n ? 'Database schema satisfies contract'\n : `Database schema does not satisfy contract (${failCount} failure${failCount === 1 ? '' : 's'})`,\n contract: {\n storageHash: input.contract.storage.storageHash,\n ...ifDefined('profileHash', profileHash),\n },\n target: {\n expected: input.contract.target,\n actual: input.contract.target,\n },\n schema: {\n issues: [...diffVerdict.failures, ...storageTypeVerdict.failures],\n warnings: {\n issues: [...diffVerdict.warnings, ...storageTypeVerdict.warnings],\n },\n },\n meta: { strict: input.strict },\n timings: { total: Date.now() - startTime },\n };\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"timestamp-now-generator-CloimujU.mjs","names":[],"sources":["../src/core/timestamp-now-generator.ts"],"sourcesContent":["import type { AuthoringFieldPresetDescriptor } from '@prisma-next/framework-components/authoring';\nimport type { MutationDefaultGeneratorDescriptor } from '@prisma-next/framework-components/control';\n\n/**\n * Canonical id for the wall-clock-now mutation default generator.\n *\n * Owned by `family-sql` because that's where the generator lives. The\n * id flows out from here to (1) the control-plane descriptor and the\n * temporal field-preset pair below, (2) the runtime-plane sibling\n * `timestamp-now-runtime-generator.ts`, and (3) authoring surfaces\n * (PSL `temporal.updatedAt()`, TS `field.temporal.updatedAt()`) via\n * the descriptor flow. Co-locating the constant with its only owner\n * keeps the framework layer free of concrete generator ids.\n */\nexport const TIMESTAMP_NOW_GENERATOR_ID = 'timestampNow' as const;\n\n/**\n * Builds the canonical control-plane descriptor for the wall-clock-now\n * mutation default generator. The descriptor's `id` and `buildPhases`\n * are target-agnostic so PSL `temporal.updatedAt()` and TS\n * `field.temporal.updatedAt()` lower to byte-identical contracts.\n *\n * `applicableCodecIds` is omitted: `timestampNow` is preset-only (not\n * reachable via `@default(timestampNow())` lowering), and the codec is\n * co-registered by the preset descriptor itself, so the\n * `@default(...)` compatibility check has no role to play here.\n */\nexport function timestampNowControlDescriptor(): MutationDefaultGeneratorDescriptor {\n return {\n id: TIMESTAMP_NOW_GENERATOR_ID,\n buildPhases: () => ({\n onCreate: { kind: 'generator', id: TIMESTAMP_NOW_GENERATOR_ID },\n onUpdate: { kind: 'generator', id: TIMESTAMP_NOW_GENERATOR_ID },\n }),\n };\n}\n\n/**\n * Builds the canonical `temporal.{createdAt,updatedAt}` field-preset pair\n * for a SQL target. `createdAt` lowers to a `now()` storage default;\n * `updatedAt` lowers to the `timestampNow` execution generator on both\n * `onCreate` and `onUpdate` (RD: \"last modified time\", non-null). Targets\n * supply the codec/native-type pair that matches their timestamp column;\n * everything else is shared so PSL `temporal.updatedAt()` and TS\n * `field.temporal.updatedAt()` lower to byte-identical contracts across\n * targets by construction.\n */\n/* @__NO_SIDE_EFFECTS__ */\nexport function temporalAuthoringPresets<\n const CodecId extends string,\n const NativeType extends string,\n>(input: { readonly codecId: CodecId; readonly nativeType: NativeType }) {\n const { codecId, nativeType } = input;\n return {\n createdAt: {\n kind: 'fieldPreset',\n output: {\n codecId,\n nativeType,\n default: { kind: 'function', expression: 'now()' },\n },\n },\n updatedAt: {\n kind: 'fieldPreset',\n output: {\n codecId,\n nativeType,\n executionDefaults: {\n onCreate: { kind: 'generator', id: TIMESTAMP_NOW_GENERATOR_ID },\n onUpdate: { kind: 'generator', id: TIMESTAMP_NOW_GENERATOR_ID },\n },\n },\n },\n } as const satisfies Record<string, AuthoringFieldPresetDescriptor>;\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAa,6BAA6B;;;;;;;;;;;;AAa1C,SAAgB,gCAAoE;CAClF,OAAO;EACL,IAAI;EACJ,oBAAoB;GAClB,UAAU;IAAE,MAAM;IAAa,IAAI;GAA2B;GAC9D,UAAU;IAAE,MAAM;IAAa,IAAI;GAA2B;EAChE;CACF;AACF;;;;;;;;;;;;AAaA,SAAgB,yBAGd,OAAuE;CACvE,MAAM,EAAE,SAAS,eAAe;CAChC,OAAO;EACL,WAAW;GACT,MAAM;GACN,QAAQ;IACN;IACA;IACA,SAAS;KAAE,MAAM;KAAY,YAAY;IAAQ;GACnD;EACF;EACA,WAAW;GACT,MAAM;GACN,QAAQ;IACN;IACA;IACA,mBAAmB;KACjB,UAAU;MAAE,MAAM;MAAa,IAAI;KAA2B;KAC9D,UAAU;MAAE,MAAM;MAAa,IAAI;KAA2B;IAChE;GACF;EACF;CACF;AACF"}