@prisma-next/family-sql 0.15.0 → 0.16.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 (49) hide show
  1. package/dist/{control-adapter-BQgad8Zc.d.mts → control-adapter-CW7FdxeS.d.mts} +1 -2
  2. package/dist/control-adapter-CW7FdxeS.d.mts.map +1 -0
  3. package/dist/control-adapter.d.mts +1 -1
  4. package/dist/control.d.mts +127 -5
  5. package/dist/control.d.mts.map +1 -1
  6. package/dist/control.mjs +71 -33
  7. package/dist/control.mjs.map +1 -1
  8. package/dist/diff.d.mts +3 -4
  9. package/dist/diff.d.mts.map +1 -1
  10. package/dist/diff.mjs +1 -1
  11. package/dist/ir.d.mts +0 -1
  12. package/dist/ir.d.mts.map +1 -1
  13. package/dist/migration.d.mts +1 -2
  14. package/dist/migration.d.mts.map +1 -1
  15. package/dist/pack.d.mts.map +1 -1
  16. package/dist/psl-infer.d.mts +2 -2
  17. package/dist/psl-infer.d.mts.map +1 -1
  18. package/dist/psl-infer.mjs +3 -4
  19. package/dist/psl-infer.mjs.map +1 -1
  20. package/dist/runtime.d.mts +0 -1
  21. package/dist/runtime.d.mts.map +1 -1
  22. package/dist/runtime.mjs +1 -1
  23. package/dist/{schema-differ-DnoopSXm.d.mts → schema-differ-C8OHo1V5.d.mts} +1 -2
  24. package/dist/schema-differ-C8OHo1V5.d.mts.map +1 -0
  25. package/dist/{schema-verify-W3r631Jh.mjs → schema-verify-DY21bMwy.mjs} +8 -8
  26. package/dist/schema-verify-DY21bMwy.mjs.map +1 -0
  27. package/dist/{timestamp-now-generator-CloimujU.mjs → timestamp-now-generator-DRXygu32.mjs} +88 -2
  28. package/dist/timestamp-now-generator-DRXygu32.mjs.map +1 -0
  29. package/dist/{types-BPv_y7iS.d.mts → types-DhAmOoxA.d.mts} +13 -7
  30. package/dist/types-DhAmOoxA.d.mts.map +1 -0
  31. package/dist/verify.d.mts +0 -1
  32. package/dist/verify.d.mts.map +1 -1
  33. package/package.json +25 -23
  34. package/src/core/diff/schema-verify.ts +5 -5
  35. package/src/core/diff/verifier-disposition.ts +3 -3
  36. package/src/core/migrations/contract-to-schema-ir.ts +96 -29
  37. package/src/core/migrations/types.ts +20 -13
  38. package/src/core/psl-contract-infer/name-transforms.ts +3 -13
  39. package/src/core/psl-contract-infer/relation-inference.ts +4 -1
  40. package/src/core/timestamp-now-generator.ts +88 -0
  41. package/src/exports/control.ts +3 -0
  42. package/dist/control-adapter-BQgad8Zc.d.mts.map +0 -1
  43. package/dist/foreign-key-index-backing-BP71iI-Q.mjs +0 -32
  44. package/dist/foreign-key-index-backing-BP71iI-Q.mjs.map +0 -1
  45. package/dist/schema-differ-DnoopSXm.d.mts.map +0 -1
  46. package/dist/schema-verify-W3r631Jh.mjs.map +0 -1
  47. package/dist/timestamp-now-generator-CloimujU.mjs.map +0 -1
  48. package/dist/types-BPv_y7iS.d.mts.map +0 -1
  49. package/src/core/foreign-key-index-backing.ts +0 -37
package/package.json CHANGED
@@ -1,35 +1,37 @@
1
1
  {
2
2
  "name": "@prisma-next/family-sql",
3
- "version": "0.15.0",
3
+ "version": "0.16.0-dev.10",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "SQL family descriptor for Prisma Next",
8
8
  "dependencies": {
9
- "@prisma-next/contract": "0.15.0",
10
- "@prisma-next/emitter": "0.15.0",
11
- "@prisma-next/framework-components": "0.15.0",
12
- "@prisma-next/migration-tools": "0.15.0",
13
- "@prisma-next/operations": "0.15.0",
14
- "@prisma-next/sql-contract": "0.15.0",
15
- "@prisma-next/sql-contract-emitter": "0.15.0",
16
- "@prisma-next/sql-contract-ts": "0.15.0",
17
- "@prisma-next/sql-operations": "0.15.0",
18
- "@prisma-next/sql-relational-core": "0.15.0",
19
- "@prisma-next/sql-runtime": "0.15.0",
20
- "@prisma-next/sql-schema-ir": "0.15.0",
21
- "@prisma-next/utils": "0.15.0",
22
- "arktype": "^2.2.2"
9
+ "@prisma-next/contract": "0.16.0-dev.10",
10
+ "@prisma-next/emitter": "0.16.0-dev.10",
11
+ "@prisma-next/framework-components": "0.16.0-dev.10",
12
+ "@prisma-next/migration-tools": "0.16.0-dev.10",
13
+ "@prisma-next/operations": "0.16.0-dev.10",
14
+ "@prisma-next/sql-contract": "0.16.0-dev.10",
15
+ "@prisma-next/sql-contract-emitter": "0.16.0-dev.10",
16
+ "@prisma-next/sql-contract-ts": "0.16.0-dev.10",
17
+ "@prisma-next/sql-operations": "0.16.0-dev.10",
18
+ "@prisma-next/sql-relational-core": "0.16.0-dev.10",
19
+ "@prisma-next/sql-runtime": "0.16.0-dev.10",
20
+ "@prisma-next/sql-schema-ir": "0.16.0-dev.10",
21
+ "@prisma-next/utils": "0.16.0-dev.10",
22
+ "arktype": "^2.2.2",
23
+ "pluralize": "^8.0.0"
23
24
  },
24
25
  "devDependencies": {
25
- "@prisma-next/driver-postgres": "0.15.0",
26
- "@prisma-next/psl-parser": "0.15.0",
27
- "@prisma-next/psl-printer": "0.15.0",
28
- "@prisma-next/sql-contract-psl": "0.15.0",
29
- "@prisma-next/test-utils": "0.15.0",
30
- "@prisma-next/tsconfig": "0.15.0",
31
- "@prisma-next/tsdown": "0.15.0",
32
- "tsdown": "0.22.3",
26
+ "@prisma-next/driver-postgres": "0.16.0-dev.10",
27
+ "@prisma-next/psl-parser": "0.16.0-dev.10",
28
+ "@prisma-next/psl-printer": "0.16.0-dev.10",
29
+ "@prisma-next/sql-contract-psl": "0.16.0-dev.10",
30
+ "@prisma-next/test-utils": "0.16.0-dev.10",
31
+ "@prisma-next/tsconfig": "0.16.0-dev.10",
32
+ "@prisma-next/tsdown": "0.16.0-dev.10",
33
+ "@types/pluralize": "^0.0.33",
34
+ "tsdown": "0.22.8",
33
35
  "typescript": "5.9.3",
34
36
  "vitest": "4.1.10"
35
37
  },
@@ -23,7 +23,7 @@ import type {
23
23
  VerifierOutcome,
24
24
  VerifyDatabaseSchemaResult,
25
25
  } from '@prisma-next/framework-components/control';
26
- import { dispositionForCategory } from '@prisma-next/framework-components/control';
26
+ import { dispositionForCategory, issueOutcome } from '@prisma-next/framework-components/control';
27
27
  import { isStorageTypeInstance, type SqlStorage } from '@prisma-next/sql-contract/types';
28
28
  import { RelationalSchemaNodeKind, type SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
29
29
  import { blindCast } from '@prisma-next/utils/casts';
@@ -101,10 +101,10 @@ export function classifySqlDiffIssue(
101
101
  issue: SchemaDiffIssue,
102
102
  granularityOf: (nodeKind: string) => DiffSubjectGranularity,
103
103
  ): VerifierIssueCategory {
104
- if (issue.reason === 'not-found') {
104
+ if (issueOutcome(issue) === 'not-found') {
105
105
  return 'declaredMissing';
106
106
  }
107
- if (issue.reason === 'not-expected') {
107
+ if (issueOutcome(issue) === 'not-expected') {
108
108
  const granularity = classifyDiffSubjectGranularity(issue, granularityOf);
109
109
  if (granularity === 'entity' || granularity === 'namespace') {
110
110
  return 'extraTopLevelObject';
@@ -173,7 +173,7 @@ export function computeSqlDiffVerdict(input: SqlDiffVerdictInput): SqlDiffVerdic
173
173
  for (const issue of input.issues) {
174
174
  if (
175
175
  !input.strict &&
176
- issue.reason === 'not-expected' &&
176
+ issueOutcome(issue) === 'not-expected' &&
177
177
  isStrictOnlyExtra(issue, input.granularityOf)
178
178
  ) {
179
179
  continue;
@@ -300,7 +300,7 @@ export function verifySqlSchemaByDiff(
300
300
  : undefined;
301
301
  return {
302
302
  ok,
303
- ...(ok ? {} : { code: 'PN-SCHEMA-0001' }),
303
+ ...(ok ? {} : { code: 'CONTRACT.SCHEMA_VERIFICATION_FAILED' }),
304
304
  summary: ok
305
305
  ? 'Database schema satisfies contract'
306
306
  : `Database schema does not satisfy contract (${failCount} failure${failCount === 1 ? '' : 's'})`,
@@ -4,7 +4,7 @@ import type {
4
4
  VerifierIssueCategory,
5
5
  VerifierOutcome,
6
6
  } from '@prisma-next/framework-components/control';
7
- import { dispositionForCategory } from '@prisma-next/framework-components/control';
7
+ import { dispositionForCategory, issueOutcome } from '@prisma-next/framework-components/control';
8
8
 
9
9
  /**
10
10
  * Classifies a codec `verifyType` hook finding into the target-neutral
@@ -13,10 +13,10 @@ import { dispositionForCategory } from '@prisma-next/framework-components/contro
13
13
  * to its value set, so a paired mismatch always classifies as `valueDrift`.
14
14
  */
15
15
  export function classifyStorageTypeDiffIssue(issue: SchemaDiffIssue): VerifierIssueCategory {
16
- if (issue.reason === 'not-found') {
16
+ if (issueOutcome(issue) === 'not-found') {
17
17
  return 'declaredMissing';
18
18
  }
19
- if (issue.reason === 'not-expected') {
19
+ if (issueOutcome(issue) === 'not-expected') {
20
20
  return 'extraAuxiliary';
21
21
  }
22
22
  return 'valueDrift';
@@ -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,
@@ -12,8 +15,8 @@ import {
12
15
  type StorageTypeInstance,
13
16
  type UniqueConstraint,
14
17
  } from '@prisma-next/sql-contract/types';
15
- import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
16
18
  import {
19
+ RelationalSchemaNodeKind,
17
20
  type SqlAnnotations,
18
21
  type SqlCheckConstraintIRInput,
19
22
  type SqlColumnIRInput,
@@ -25,7 +28,6 @@ import {
25
28
  } from '@prisma-next/sql-schema-ir/types';
26
29
  import { blindCast } from '@prisma-next/utils/casts';
27
30
  import { ifDefined } from '@prisma-next/utils/defined';
28
- import { backingIndexColumnKeys } from '../foreign-key-index-backing';
29
31
 
30
32
  /**
31
33
  * Target-specific callback that expands a column's base `nativeType` and optional
@@ -54,6 +56,17 @@ export type NativeTypeExpander = (input: {
54
56
  */
55
57
  export type DefaultRenderer = (def: ColumnDefault, column: StorageColumn) => string;
56
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
+
57
70
  /**
58
71
  * Target-supplied callback that resolves a contract namespace to the live
59
72
  * database schema its enums are stored under.
@@ -75,6 +88,7 @@ function convertColumn(
75
88
  storageTypes: ResolvedStorageTypes,
76
89
  expandNativeType: NativeTypeExpander | undefined,
77
90
  renderDefault: DefaultRenderer | undefined,
91
+ resolveDefault: DefaultResolver | undefined,
78
92
  ): SqlColumnIRInput {
79
93
  // Resolve `typeRef` so columns that delegate their `nativeType`/`codecId`/
80
94
  // `typeParams` to a named `storage.types` entry expand the same way as
@@ -105,6 +119,11 @@ function convertColumn(
105
119
  // agree on as the comparable "expanded" type.
106
120
  const nativeType = baseNativeType;
107
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;
108
127
  return {
109
128
  name,
110
129
  nativeType,
@@ -115,11 +134,14 @@ function convertColumn(
115
134
  column.default != null && renderDefault ? renderDefault(column.default, column) : undefined,
116
135
  ),
117
136
  // Contract-derived columns are resolved by construction: the computed
118
- // full native type doubles as the resolved value, and the contract's
119
- // structured default is the resolved default (the introspected side
120
- // 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.
121
143
  resolvedNativeType,
122
- ...ifDefined('resolvedDefault', column.default ?? undefined),
144
+ ...ifDefined('resolvedDefault', resolvedColumnDefault),
123
145
  // The column's codec identity, carried the same way the query AST
124
146
  // carries `CodecRef` (TML-2456) — the migration planner's op-builders
125
147
  // resolve DDL rendering from this at plan time (Decision 5), instead of
@@ -246,14 +268,15 @@ function convertCheck(check: CheckConstraint, storage: SqlStorage): SqlCheckCons
246
268
  };
247
269
  }
248
270
 
249
- function convertUnique(unique: UniqueConstraint): SqlUniqueIRInput {
271
+ function convertUnique(unique: UniqueConstraint, tableName: string): SqlUniqueIRInput {
250
272
  return {
251
273
  columns: unique.columns,
252
274
  ...ifDefined('name', unique.name),
275
+ dependsOn: flatColumnDependsOn(tableName, unique.columns),
253
276
  };
254
277
  }
255
278
 
256
- function convertIndex(index: Index): SqlIndexIRInput {
279
+ function convertIndex(index: Index, tableName: string): SqlIndexIRInput {
257
280
  return {
258
281
  columns: index.columns,
259
282
  unique: false,
@@ -262,9 +285,42 @@ function convertIndex(index: Index): SqlIndexIRInput {
262
285
  // introspected side (the legacy walk read them from the contract).
263
286
  ...ifDefined('type', index.type),
264
287
  ...ifDefined('options', index.options),
288
+ dependsOn: flatColumnDependsOn(tableName, index.columns),
265
289
  };
266
290
  }
267
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
+
268
324
  /**
269
325
  * The FK's referenced-namespace identity comes from the target's namespace
270
326
  * node, not the raw namespace-id string. An unbound target namespace stamps
@@ -274,6 +330,10 @@ function convertIndex(index: Index): SqlIndexIRInput {
274
330
  * cross-space target whose namespace lives in another contract's storage)
275
331
  * stamps its coordinate verbatim; namespaced targets (Postgres) resolve the
276
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).
277
337
  */
278
338
  function convertForeignKey(fk: ForeignKey, storage: SqlStorage): SqlForeignKeyIRInput {
279
339
  const targetNamespace = storage.namespaces[fk.target.namespaceId];
@@ -286,6 +346,10 @@ function convertForeignKey(fk: ForeignKey, storage: SqlStorage): SqlForeignKeyIR
286
346
  ...ifDefined('name', fk.name),
287
347
  ...ifDefined('onDelete', fk.onDelete),
288
348
  ...ifDefined('onUpdate', fk.onUpdate),
349
+ dependsOn: [
350
+ flatSchemaDependsOn(fk.target.tableName),
351
+ ...flatColumnDependsOn(fk.source.tableName, fk.source.columns),
352
+ ],
289
353
  };
290
354
  }
291
355
 
@@ -295,6 +359,7 @@ function convertTable(
295
359
  storageTypes: ResolvedStorageTypes,
296
360
  expandNativeType: NativeTypeExpander | undefined,
297
361
  renderDefault: DefaultRenderer | undefined,
362
+ resolveDefault: DefaultResolver | undefined,
298
363
  storage: SqlStorage,
299
364
  ): SqlTableIR {
300
365
  const columns: Record<string, SqlColumnIRInput> = {};
@@ -305,37 +370,36 @@ function convertTable(
305
370
  storageTypes,
306
371
  expandNativeType,
307
372
  renderDefault,
373
+ resolveDefault,
308
374
  );
309
375
  }
310
376
 
311
- const satisfiedIndexColumns = new Set(backingIndexColumnKeys(table));
312
- const fkBackingIndexes: SqlIndexIRInput[] = [];
313
- for (const fk of table.foreignKeys) {
314
- if (fk.index === false) continue;
315
- const key = fk.source.columns.join(',');
316
- if (satisfiedIndexColumns.has(key)) continue;
317
- fkBackingIndexes.push({
318
- columns: fk.source.columns,
319
- unique: false,
320
- name: defaultIndexName(name, fk.source.columns),
321
- });
322
- satisfiedIndexColumns.add(key);
323
- }
324
-
325
377
  const checks: SqlCheckConstraintIRInput[] | undefined =
326
378
  table.checks && table.checks.length > 0
327
379
  ? table.checks.map((c) => convertCheck(c, storage))
328
380
  : undefined;
329
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
+
330
391
  return new SqlTableIR({
331
392
  name,
332
393
  columns,
333
- ...ifDefined('primaryKey', table.primaryKey),
334
- foreignKeys: table.foreignKeys
335
- .filter((fk) => fk.constraint !== false)
336
- .map((fk) => convertForeignKey(fk, storage)),
337
- uniques: table.uniques.map(convertUnique),
338
- indexes: [...table.indexes.map(convertIndex), ...fkBackingIndexes],
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.
400
+ foreignKeys: table.foreignKeys.map((fk) => convertForeignKey(fk, storage)),
401
+ uniques: table.uniques.map((u) => convertUnique(u, name)),
402
+ indexes: table.indexes.map((i) => convertIndex(i, name)),
339
403
  ...ifDefined('checks', checks),
340
404
  });
341
405
  }
@@ -402,6 +466,7 @@ export interface ContractToSchemaIROptions {
402
466
  readonly annotationNamespace: string;
403
467
  readonly expandNativeType?: NativeTypeExpander;
404
468
  readonly renderDefault?: DefaultRenderer;
469
+ readonly resolveDefault?: DefaultResolver;
405
470
  /**
406
471
  * Target-supplied resolver mapping a namespace to the live database schema
407
472
  * its enums are stored under. When provided (Postgres), namespace-scoped
@@ -462,6 +527,7 @@ export function contractNamespaceToSchemaIR(
462
527
  storageTypes,
463
528
  options.expandNativeType,
464
529
  options.renderDefault,
530
+ options.resolveDefault,
465
531
  storage,
466
532
  );
467
533
  }
@@ -498,6 +564,7 @@ export function contractToSchemaIR(
498
564
  storageTypes,
499
565
  options.expandNativeType,
500
566
  options.renderDefault,
567
+ options.resolveDefault,
501
568
  storage,
502
569
  );
503
570
  }
@@ -334,9 +334,9 @@ export interface SqlMigrationPlannerPlanOptions {
334
334
  * or `null` for reconciliation flows that have no prior contract.
335
335
  *
336
336
  * Required at every call site so the structural fact "I have a prior
337
- * contract / I don't" is visible in the type. `migration plan` reads
338
- * the predecessor bundle's `end-contract.json` from disk and passes
339
- * the parsed value; `db update` / `db init` reconcile against the
337
+ * contract / I don't" is visible in the type. `migration plan` reads the
338
+ * predecessor's contract from the snapshot store by its storage hash and
339
+ * passes the parsed value; `db update` / `db init` reconcile against the
340
340
  * live schema and pass `null`. Strategies that
341
341
  * need from/to column-shape comparisons (unsafe type change, nullability
342
342
  * tightening) use this to decide whether to emit `dataTransform`
@@ -346,6 +346,13 @@ export interface SqlMigrationPlannerPlanOptions {
346
346
  * plan's `describe()` as `fromContract?.storage.storageHash ?? null`.
347
347
  */
348
348
  readonly fromContract: Contract<SqlStorage> | null;
349
+ /**
350
+ * POSIX-relative path from the migration package dir to
351
+ * `migrations/snapshots`, e.g. `'../../snapshots'`. Threaded straight
352
+ * into the produced plan's `renderTypeScript()` metadata so the
353
+ * scaffold's contract-snapshot imports point at the deduplicated store.
354
+ */
355
+ readonly snapshotsImportPath: string;
349
356
  /**
350
357
  * Active framework components participating in this composition.
351
358
  * Each component is target-bound so SQL targets can dispatch
@@ -419,16 +426,16 @@ export interface SqlMigrationRunnerExecuteOptions<TTargetDetails> {
419
426
  }
420
427
 
421
428
  export type SqlMigrationRunnerErrorCode =
422
- | 'DESTINATION_CONTRACT_MISMATCH'
423
- | 'LEGACY_MARKER_SHAPE'
424
- | 'MARKER_ORIGIN_MISMATCH'
425
- | 'MARKER_CAS_FAILURE'
426
- | 'POLICY_VIOLATION'
427
- | 'PRECHECK_FAILED'
428
- | 'POSTCHECK_FAILED'
429
- | 'SCHEMA_VERIFY_FAILED'
430
- | 'FOREIGN_KEY_VIOLATION'
431
- | 'EXECUTION_FAILED';
429
+ | 'MIGRATION.DESTINATION_CONTRACT_MISMATCH'
430
+ | 'MIGRATION.LEGACY_MARKER_SHAPE'
431
+ | 'MIGRATION.MARKER_ORIGIN_MISMATCH'
432
+ | 'MIGRATION.MARKER_CAS_FAILURE'
433
+ | 'MIGRATION.POLICY_VIOLATION'
434
+ | 'MIGRATION.PRECHECK_FAILED'
435
+ | 'MIGRATION.POSTCHECK_FAILED'
436
+ | 'MIGRATION.SCHEMA_VERIFY_FAILED'
437
+ | 'MIGRATION.FOREIGN_KEY_VIOLATION'
438
+ | 'MIGRATION.EXECUTION_FAILED';
432
439
 
433
440
  export interface SqlMigrationRunnerFailure extends MigrationRunnerFailure {
434
441
  readonly code: SqlMigrationRunnerErrorCode;
@@ -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(
@@ -1,5 +1,8 @@
1
+ import {
2
+ backingIndexColumnKeys,
3
+ isBackedByColumnKeys,
4
+ } from '@prisma-next/sql-contract/foreign-key-materialization';
1
5
  import type { SqlForeignKeyIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
2
- import { backingIndexColumnKeys, isBackedByColumnKeys } from '../foreign-key-index-backing';
3
6
  import { deriveBackRelationFieldName, deriveRelationFieldName, pluralize } from './name-transforms';
4
7
  import type { RelationField } from './printer-config';
5
8
 
@@ -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":"control-adapter-BQgad8Zc.d.mts","names":[],"sources":["../src/core/diff/sql-schema-diff.ts","../src/core/control-adapter.ts"],"mappings":";;;;;;;;;;;KAeY,iBAAA,IACV,UAAA,UACA,UAAA,aACG,aAAa;;AAAA;AAOlB;;;KAAY,oBAAA,IAAwB,UAAkB;AAAA;AAKtD;;AALsD,iBAKtC,WAAA,CAAY,CAAA,qBAAsB,CAAoB;;;;AAftE;;;;;;UCQiB,OAAA;EACf,KAAA,CAAM,GAAA,EAAK,WAAA,GAAc,OAAA,EAAS,OAAA,EAAS,cAAA,YAA0B,gBAAA;AAAA;ADCvE;;;;AAAsD;AAKtD;AALA,UCQiB,qBAAA,SAA8B,OAAA;EAC7C,qBAAA,CACE,GAAA,EAAK,WAAA,GAAc,OAAA,EACnB,OAAA,GAAU,cAAA,YACT,OAAA,CAAQ,iBAAA;AAAA;ADPyD;;;;ACPtE;;ADOsE,UCgBrD,iBAAA,0CACP,sBAAA,QAA8B,OAAA,GACpC,qBAAA;EAxBS;;;;;;;;;;;;;;AAA0E;EAwCrF,UAAA,CACE,MAAA,EAAQ,wBAAA,CAAyB,OAAA,GACjC,KAAA,WACC,OAAA,CAAQ,oBAAA;EAlC0B;;;;;;;EA2CrC,cAAA,CACE,MAAA,EAAQ,wBAAA,CAAyB,OAAA,IAChC,OAAA,CAAQ,WAAA,SAAoB,oBAAA;EA7CqB;;;;EAmDpD,UAAA,CACE,MAAA,EAAQ,wBAAA,CAAyB,OAAA,GACjC,KAAA,YACC,OAAA,UAAiB,iBAAA;EApDC;;;;;;EA4DrB,YAAA,CACE,MAAA,EAAQ,wBAAA,CAAyB,OAAA,GACjC,KAAA,UACA,WAAA;IAAA,SACW,WAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EAxDmC;;;;;;EAgEtC,UAAA,CACE,MAAA,EAAQ,wBAAA,CAAyB,OAAA,GACjC,KAAA,UACA,WAAA;IAAA,SACW,WAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EAlCO;;;;;;;EA2CV,YAAA,CACE,MAAA,EAAQ,wBAAA,CAAyB,OAAA,GACjC,KAAA,UACA,YAAA,UACA,WAAA;IAAA,SACW,WAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EASgC;;;;;;EADnC,gBAAA,CACE,MAAA,EAAQ,wBAAA,CAAyB,OAAA,GACjC,KAAA,UACA,KAAA;IAAA,SACW,MAAA;IAAA,SACA,IAAA;IAAA,SACA,EAAA;IAAA,SACA,aAAA;IAAA,SACA,aAAA;IAAA,SACA,UAAA;IAAA,SACA,uBAAA;EAAA,IAEV,OAAA;EA/G8B;;;;;;;;;;;;;;;;;EAkIjC,UAAA,CACE,MAAA,EAAQ,wBAAA,CAAyB,OAAA,GACjC,QAAA,YACA,MAAA,YACC,OAAA,CAAQ,eAAA;EAhGX;;;;;EAAA,SAuGS,gBAAA,GAAmB,iBAAA;EApGR;;;;;EAAA,SA2GX,mBAAA,GAAsB,oBAAA;EA/FlB;;;;EAqGb,4BAAA,aAAyC,OAAA;EAzFzC;;;;EA+FA,0BAAA,aAAuC,OAAA;AAAA;;;;;;;UASxB,2BAAA;EAtFb;;;;;;EA6FF,MAAA,CAAO,KAAA,EAAO,YAAA,QAAoB,OAAA,IAAW,iBAAA,CAAkB,OAAA;AAAA"}
@@ -1,32 +0,0 @@
1
- //#region src/core/foreign-key-index-backing.ts
2
- /**
3
- * The column-list keys (`"colA,colB"`, order preserved) a table's own
4
- * indexes, unique constraints, and primary key already back. A foreign key
5
- * whose source columns join to one of these keys needs no separately
6
- * derived backing index.
7
- *
8
- * Shared by {@link isBackedByColumnKeys}'s two callers, which must agree on
9
- * what counts as "backed": `contract-to-schema-ir.ts`'s `convertTable`
10
- * (deriving the FK-backing-index expectation `db verify` checks against a
11
- * live database) and the postgres PSL inferrer (deciding whether an
12
- * introspected relation needs an explicit `index: false`).
13
- */
14
- function backingIndexColumnKeys(table) {
15
- return [
16
- ...table.indexes.map((index) => index.columns.join(",")),
17
- ...table.uniques.map((unique) => unique.columns.join(",")),
18
- ...table.primaryKey ? [table.primaryKey.columns.join(",")] : []
19
- ];
20
- }
21
- /**
22
- * Whether `columns`, in order, matches one of `backingKeys` (see
23
- * {@link backingIndexColumnKeys}). Order-sensitive: `(a, b)` does not
24
- * satisfy a backing index declared as `(b, a)`.
25
- */
26
- function isBackedByColumnKeys(columns, backingKeys) {
27
- return backingKeys.includes(columns.join(","));
28
- }
29
- //#endregion
30
- export { isBackedByColumnKeys as n, backingIndexColumnKeys as t };
31
-
32
- //# sourceMappingURL=foreign-key-index-backing-BP71iI-Q.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"foreign-key-index-backing-BP71iI-Q.mjs","names":[],"sources":["../src/core/foreign-key-index-backing.ts"],"sourcesContent":["export type BackingIndexCandidates = {\n readonly indexes: readonly { readonly columns: readonly string[] }[];\n readonly uniques: readonly { readonly columns: readonly string[] }[];\n readonly primaryKey?: { readonly columns: readonly string[] } | undefined;\n};\n\n/**\n * The column-list keys (`\"colA,colB\"`, order preserved) a table's own\n * indexes, unique constraints, and primary key already back. A foreign key\n * whose source columns join to one of these keys needs no separately\n * derived backing index.\n *\n * Shared by {@link isBackedByColumnKeys}'s two callers, which must agree on\n * what counts as \"backed\": `contract-to-schema-ir.ts`'s `convertTable`\n * (deriving the FK-backing-index expectation `db verify` checks against a\n * live database) and the postgres PSL inferrer (deciding whether an\n * introspected relation needs an explicit `index: false`).\n */\nexport function backingIndexColumnKeys(table: BackingIndexCandidates): readonly string[] {\n return [\n ...table.indexes.map((index) => index.columns.join(',')),\n ...table.uniques.map((unique) => unique.columns.join(',')),\n ...(table.primaryKey ? [table.primaryKey.columns.join(',')] : []),\n ];\n}\n\n/**\n * Whether `columns`, in order, matches one of `backingKeys` (see\n * {@link backingIndexColumnKeys}). Order-sensitive: `(a, b)` does not\n * satisfy a backing index declared as `(b, a)`.\n */\nexport function isBackedByColumnKeys(\n columns: readonly string[],\n backingKeys: readonly string[],\n): boolean {\n return backingKeys.includes(columns.join(','));\n}\n"],"mappings":";;;;;;;;;;;;;AAkBA,SAAgB,uBAAuB,OAAkD;CACvF,OAAO;EACL,GAAG,MAAM,QAAQ,KAAK,UAAU,MAAM,QAAQ,KAAK,GAAG,CAAC;EACvD,GAAG,MAAM,QAAQ,KAAK,WAAW,OAAO,QAAQ,KAAK,GAAG,CAAC;EACzD,GAAI,MAAM,aAAa,CAAC,MAAM,WAAW,QAAQ,KAAK,GAAG,CAAC,IAAI,CAAC;CACjE;AACF;;;;;;AAOA,SAAgB,qBACd,SACA,aACS;CACT,OAAO,YAAY,SAAS,QAAQ,KAAK,GAAG,CAAC;AAC/C"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"schema-differ-DnoopSXm.d.mts","names":[],"sources":["../src/core/migrations/schema-differ.ts"],"mappings":";;;;;;;;;AAcA;;;;;;UAAiB,mBAAA;EAiBU;EAAA,SAfhB,MAAA,WAAiB,eAAA;EAeY;;;;;;;;EAAA,SAN7B,oBAAA,GAAuB,KAAA,EAAO,eAAA,KAAoB,aAAA;EAMT;;;AAAuB;AAG3E;EAHoD,SAAzC,cAAA,EAAgB,aAAA;IAAA,SAAyB,MAAA,EAAQ,eAAA;EAAA;AAAA;AAAA,UAG3C,kBAAA;EAAA,SACN,QAAA,EAAU,QAAA,CAAS,UAAA;EAAA,SACnB,MAAA,EAAQ,eAAA;EAAA,SACR,mBAAA,EAAqB,aAAA,CAAc,8BAAA;AAAA;AAAA,KAGlC,eAAA,IAAmB,KAAA,EAAO,kBAAA,KAAuB,mBAAmB"}