@prisma-next/family-sql 0.5.0-dev.6 → 0.5.0-dev.61

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 (58) hide show
  1. package/README.md +2 -2
  2. package/dist/{authoring-type-constructors-BAR65pSK.mjs → authoring-type-constructors-D9b8AG6y.mjs} +13 -13
  3. package/dist/authoring-type-constructors-D9b8AG6y.mjs.map +1 -0
  4. package/dist/control-adapter.d.mts +12 -1
  5. package/dist/control-adapter.d.mts.map +1 -1
  6. package/dist/control.d.mts +97 -27
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +1030 -13
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/migration.d.mts +22 -23
  11. package/dist/migration.d.mts.map +1 -1
  12. package/dist/migration.mjs +24 -22
  13. package/dist/migration.mjs.map +1 -1
  14. package/dist/pack.d.mts +35 -23
  15. package/dist/pack.mjs +1 -1
  16. package/dist/runtime.d.mts +19 -1
  17. package/dist/runtime.d.mts.map +1 -1
  18. package/dist/runtime.mjs +27 -1
  19. package/dist/runtime.mjs.map +1 -1
  20. package/dist/schema-verify.d.mts +2 -2
  21. package/dist/timestamp-now-generator-Bt4L3cfM.mjs +86 -0
  22. package/dist/timestamp-now-generator-Bt4L3cfM.mjs.map +1 -0
  23. package/dist/{types-C6K4mxDM.d.mts → types-DiBtjG6M.d.mts} +46 -22
  24. package/dist/types-DiBtjG6M.d.mts.map +1 -0
  25. package/dist/verify-BdES8wgQ.mjs +82 -0
  26. package/dist/verify-BdES8wgQ.mjs.map +1 -0
  27. package/dist/{verify-sql-schema-BBhkqEDo.d.mts → verify-sql-schema-DW8Agf1F.d.mts} +2 -2
  28. package/dist/{verify-sql-schema-BBhkqEDo.d.mts.map → verify-sql-schema-DW8Agf1F.d.mts.map} +1 -1
  29. package/dist/verify-sql-schema-Ovz7RXR5.mjs.map +1 -1
  30. package/dist/verify.d.mts +16 -20
  31. package/dist/verify.d.mts.map +1 -1
  32. package/dist/verify.mjs +2 -2
  33. package/package.json +19 -18
  34. package/src/core/authoring-field-presets.ts +35 -23
  35. package/src/core/control-adapter.ts +12 -0
  36. package/src/core/control-instance.ts +43 -15
  37. package/src/core/migrations/plan-helpers.ts +3 -0
  38. package/src/core/migrations/types.ts +38 -20
  39. package/src/core/operation-preview.ts +62 -0
  40. package/src/core/psl-contract-infer/default-mapping.ts +56 -0
  41. package/src/core/psl-contract-infer/name-transforms.ts +178 -0
  42. package/src/core/psl-contract-infer/postgres-default-mapping.ts +16 -0
  43. package/src/core/psl-contract-infer/postgres-type-map.ts +165 -0
  44. package/src/core/psl-contract-infer/printer-config.ts +55 -0
  45. package/src/core/psl-contract-infer/raw-default-parser.ts +91 -0
  46. package/src/core/psl-contract-infer/relation-inference.ts +196 -0
  47. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +832 -0
  48. package/src/core/sql-migration.ts +25 -23
  49. package/src/core/timestamp-now-generator.ts +74 -0
  50. package/src/core/timestamp-now-runtime-generator.ts +24 -0
  51. package/src/core/verify.ts +46 -108
  52. package/src/exports/control.ts +4 -1
  53. package/src/exports/runtime.ts +2 -0
  54. package/src/exports/verify.ts +1 -1
  55. package/dist/authoring-type-constructors-BAR65pSK.mjs.map +0 -1
  56. package/dist/types-C6K4mxDM.d.mts.map +0 -1
  57. package/dist/verify-4GshvY4p.mjs +0 -122
  58. package/dist/verify-4GshvY4p.mjs.map +0 -1
@@ -99,9 +99,11 @@ export const sqlFamilyAuthoringFieldPresets = {
99
99
  typeParams: {
100
100
  length: 36,
101
101
  },
102
- executionDefault: {
103
- kind: 'generator',
104
- id: 'uuidv4',
102
+ executionDefaults: {
103
+ onCreate: {
104
+ kind: 'generator',
105
+ id: 'uuidv4',
106
+ },
105
107
  },
106
108
  id: true,
107
109
  },
@@ -114,9 +116,11 @@ export const sqlFamilyAuthoringFieldPresets = {
114
116
  typeParams: {
115
117
  length: 36,
116
118
  },
117
- executionDefault: {
118
- kind: 'generator',
119
- id: 'uuidv7',
119
+ executionDefaults: {
120
+ onCreate: {
121
+ kind: 'generator',
122
+ id: 'uuidv7',
123
+ },
120
124
  },
121
125
  id: true,
122
126
  },
@@ -129,9 +133,11 @@ export const sqlFamilyAuthoringFieldPresets = {
129
133
  typeParams: {
130
134
  length: 26,
131
135
  },
132
- executionDefault: {
133
- kind: 'generator',
134
- id: 'ulid',
136
+ executionDefaults: {
137
+ onCreate: {
138
+ kind: 'generator',
139
+ id: 'ulid',
140
+ },
135
141
  },
136
142
  id: true,
137
143
  },
@@ -150,14 +156,16 @@ export const sqlFamilyAuthoringFieldPresets = {
150
156
  default: 21,
151
157
  },
152
158
  },
153
- executionDefault: {
154
- kind: 'generator',
155
- id: 'nanoid',
156
- params: {
157
- size: {
158
- kind: 'arg',
159
- index: 0,
160
- path: ['size'],
159
+ executionDefaults: {
160
+ onCreate: {
161
+ kind: 'generator',
162
+ id: 'nanoid',
163
+ params: {
164
+ size: {
165
+ kind: 'arg',
166
+ index: 0,
167
+ path: ['size'],
168
+ },
161
169
  },
162
170
  },
163
171
  },
@@ -172,9 +180,11 @@ export const sqlFamilyAuthoringFieldPresets = {
172
180
  typeParams: {
173
181
  length: 24,
174
182
  },
175
- executionDefault: {
176
- kind: 'generator',
177
- id: 'cuid2',
183
+ executionDefaults: {
184
+ onCreate: {
185
+ kind: 'generator',
186
+ id: 'cuid2',
187
+ },
178
188
  },
179
189
  id: true,
180
190
  },
@@ -187,9 +197,11 @@ export const sqlFamilyAuthoringFieldPresets = {
187
197
  typeParams: {
188
198
  length: 27,
189
199
  },
190
- executionDefault: {
191
- kind: 'generator',
192
- id: 'ksuid',
200
+ executionDefaults: {
201
+ onCreate: {
202
+ kind: 'generator',
203
+ id: 'ksuid',
204
+ },
193
205
  },
194
206
  id: true,
195
207
  },
@@ -1,3 +1,4 @@
1
+ import type { ContractMarkerRecord } from '@prisma-next/contract/types';
1
2
  import type {
2
3
  ControlAdapterInstance,
3
4
  ControlDriverInstance,
@@ -19,6 +20,17 @@ import type { DefaultNormalizer, NativeTypeNormalizer } from './schema-verify/ve
19
20
  */
20
21
  export interface SqlControlAdapter<TTarget extends string = string>
21
22
  extends ControlAdapterInstance<'sql', TTarget> {
23
+ /**
24
+ * Reads the contract marker from the database, returning `null` if the marker
25
+ * table or its row is missing. Implementations are responsible for the
26
+ * dialect-specific existence probe (e.g. Postgres `information_schema.tables`
27
+ * vs SQLite `sqlite_master`) and parameter placeholders.
28
+ *
29
+ * @param driver - ControlDriverInstance for executing queries (target-specific)
30
+ * @returns Resolved marker record, or `null` if not yet stamped.
31
+ */
32
+ readMarker(driver: ControlDriverInstance<'sql', TTarget>): Promise<ContractMarkerRecord | null>;
33
+
22
34
  /**
23
35
  * Introspects a database schema and returns a raw SqlSchemaIR.
24
36
  *
@@ -9,7 +9,11 @@ import type {
9
9
  ControlFamilyInstance,
10
10
  ControlStack,
11
11
  CoreSchemaView,
12
+ MigrationPlanOperation,
12
13
  OperationContext,
14
+ OperationPreview,
15
+ OperationPreviewCapable,
16
+ PslContractInferCapable,
13
17
  SchemaViewCapable,
14
18
  SignDatabaseResult,
15
19
  VerifyDatabaseResult,
@@ -22,6 +26,7 @@ import {
22
26
  VERIFY_CODE_TARGET_MISMATCH,
23
27
  } from '@prisma-next/framework-components/control';
24
28
  import type { TypesImportSpec } from '@prisma-next/framework-components/emission';
29
+ import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
25
30
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
26
31
  import { validateContract as sqlValidateContract } from '@prisma-next/sql-contract/validate';
27
32
  import {
@@ -37,8 +42,10 @@ import type {
37
42
  SqlControlAdapterDescriptor,
38
43
  SqlControlExtensionDescriptor,
39
44
  } from './migrations/types';
45
+ import { sqlOperationsToPreview } from './operation-preview';
46
+ import { sqlSchemaIrToPslAst } from './psl-contract-infer/sql-schema-ir-to-psl-ast';
40
47
  import { verifySqlSchema } from './schema-verify/verify-sql-schema';
41
- import { collectSupportedCodecTypeIds, readMarker } from './verify';
48
+ import { collectSupportedCodecTypeIds } from './verify';
42
49
 
43
50
  function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
44
51
  const typeIds = new Set<string>();
@@ -182,6 +189,8 @@ export interface SchemaVerifyOptions {
182
189
  export interface SqlControlFamilyInstance
183
190
  extends ControlFamilyInstance<'sql', SqlSchemaIR>,
184
191
  SchemaViewCapable<SqlSchemaIR>,
192
+ PslContractInferCapable<SqlSchemaIR>,
193
+ OperationPreviewCapable,
185
194
  SqlFamilyInstanceState {
186
195
  validateContract(contractJson: unknown): Contract;
187
196
 
@@ -206,6 +215,10 @@ export interface SqlControlFamilyInstance
206
215
  readonly driver: ControlDriverInstance<'sql', string>;
207
216
  readonly contract?: unknown;
208
217
  }): Promise<SqlSchemaIR>;
218
+
219
+ inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst;
220
+
221
+ toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview;
209
222
  }
210
223
 
211
224
  export type SqlFamilyInstance = SqlControlFamilyInstance;
@@ -217,7 +230,9 @@ function isSqlControlAdapter<TTargetId extends string>(
217
230
  typeof value === 'object' &&
218
231
  value !== null &&
219
232
  'introspect' in value &&
220
- typeof (value as { introspect: unknown }).introspect === 'function'
233
+ typeof (value as { introspect: unknown }).introspect === 'function' &&
234
+ 'readMarker' in value &&
235
+ typeof (value as { readMarker: unknown }).readMarker === 'function'
221
236
  );
222
237
  }
223
238
 
@@ -293,6 +308,20 @@ export function createSqlFamilyInstance<TTargetId extends string>(
293
308
  extensionPacks: extensions,
294
309
  });
295
310
 
311
+ // Family-instance methods accept `ControlDriverInstance<'sql', string>` —
312
+ // the family API isn't generic on the target id. Letting `isSqlControlAdapter`
313
+ // default its type parameter narrows the adapter to `SqlControlAdapter<string>`,
314
+ // which matches the family-level driver type without any cast at call sites.
315
+ const getControlAdapter = () => {
316
+ const controlAdapter = adapter.create(stack);
317
+ if (!isSqlControlAdapter(controlAdapter)) {
318
+ throw new Error(
319
+ 'Adapter does not implement SqlControlAdapter (missing introspect or readMarker)',
320
+ );
321
+ }
322
+ return controlAdapter;
323
+ };
324
+
296
325
  return {
297
326
  familyId: 'sql',
298
327
  codecTypeImports,
@@ -326,7 +355,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
326
355
  const contractProfileHash = contract.profileHash;
327
356
  const contractTarget = contract.target;
328
357
 
329
- const marker = await readMarker(driver);
358
+ const marker = await getControlAdapter().readMarker(driver);
330
359
 
331
360
  let missingCodecs: readonly string[] | undefined;
332
361
  let codecCoverageSkipped = false;
@@ -435,10 +464,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
435
464
 
436
465
  const contract = sqlValidateContract<Contract<SqlStorage>>(contractInput, emptyCodecLookup);
437
466
 
438
- const controlAdapter = adapter.create(stack);
439
- if (!isSqlControlAdapter(controlAdapter)) {
440
- throw new Error('Adapter does not implement SqlControlAdapter.introspect()');
441
- }
467
+ const controlAdapter = getControlAdapter();
442
468
  const schemaIR = await controlAdapter.introspect(driver, contractInput);
443
469
 
444
470
  return verifySqlSchema({
@@ -474,7 +500,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
474
500
  await driver.query(ensureSchemaStatement.sql, ensureSchemaStatement.params);
475
501
  await driver.query(ensureTableStatement.sql, ensureTableStatement.params);
476
502
 
477
- const existingMarker = await readMarker(driver);
503
+ const existingMarker = await getControlAdapter().readMarker(driver);
478
504
 
479
505
  let markerCreated = false;
480
506
  let markerUpdated = false;
@@ -551,19 +577,21 @@ export function createSqlFamilyInstance<TTargetId extends string>(
551
577
  async readMarker(options: {
552
578
  readonly driver: ControlDriverInstance<'sql', string>;
553
579
  }): Promise<ContractMarkerRecord | null> {
554
- return readMarker(options.driver);
580
+ return getControlAdapter().readMarker(options.driver);
555
581
  },
556
582
  async introspect(options: {
557
583
  readonly driver: ControlDriverInstance<'sql', string>;
558
584
  readonly contract?: unknown;
559
585
  }): Promise<SqlSchemaIR> {
560
- const { driver, contract } = options;
586
+ return getControlAdapter().introspect(options.driver, options.contract);
587
+ },
561
588
 
562
- const controlAdapter = adapter.create(stack);
563
- if (!isSqlControlAdapter(controlAdapter)) {
564
- throw new Error('Adapter does not implement SqlControlAdapter.introspect()');
565
- }
566
- return controlAdapter.introspect(driver, contract);
589
+ inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst {
590
+ return sqlSchemaIrToPslAst(schemaIR);
591
+ },
592
+
593
+ toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview {
594
+ return sqlOperationsToPreview(operations);
567
595
  },
568
596
 
569
597
  toSchemaView(schema: SqlSchemaIR): CoreSchemaView {
@@ -35,6 +35,7 @@ function freezeSteps(
35
35
  Object.freeze({
36
36
  description: step.description,
37
37
  sql: step.sql,
38
+ ...(step.params ? { params: Object.freeze([...step.params]) } : {}),
38
39
  ...(step.meta ? { meta: cloneRecord(step.meta) } : {}),
39
40
  }),
40
41
  ),
@@ -74,6 +75,7 @@ function freezeOperation<TTargetDetails>(
74
75
  label: operation.label,
75
76
  ...(operation.summary ? { summary: operation.summary } : {}),
76
77
  operationClass: operation.operationClass,
78
+ ...(operation.invariantId ? { invariantId: operation.invariantId } : {}),
77
79
  target: freezeTargetDetails(operation.target),
78
80
  precheck: freezeSteps(operation.precheck),
79
81
  execute: freezeSteps(operation.execute),
@@ -101,6 +103,7 @@ export function createMigrationPlan<TTargetDetails>(
101
103
  : {}),
102
104
  destination: Object.freeze({ ...options.destination }),
103
105
  operations: freezeOperations(options.operations),
106
+ providedInvariants: Object.freeze([...options.providedInvariants]),
104
107
  ...(options.meta ? { meta: cloneRecord(options.meta) } : {}),
105
108
  });
106
109
  }
@@ -4,7 +4,6 @@ import type {
4
4
  ControlAdapterDescriptor,
5
5
  ControlDriverInstance,
6
6
  ControlExtensionDescriptor,
7
- DataTransformOperation,
8
7
  MigratableTargetDescriptor,
9
8
  MigrationOperationPolicy,
10
9
  MigrationPlan,
@@ -139,6 +138,15 @@ export interface SqlControlAdapterDescriptor<TTargetId extends string>
139
138
  export interface SqlMigrationPlanOperationStep {
140
139
  readonly description: string;
141
140
  readonly sql: string;
141
+ /**
142
+ * Optional parameter values bound at execution time. The runner forwards
143
+ * these to `driver.query(sql, params ?? [])`, so step authors can use
144
+ * placeholder syntax (`$1`, `$2`, …) instead of inlining literals into
145
+ * the SQL string. Reuses the driver's parameter binder rather than
146
+ * rolling per-target literal serialization for every type the planner
147
+ * may emit.
148
+ */
149
+ readonly params?: readonly unknown[];
142
150
  readonly meta?: AnyRecord;
143
151
  }
144
152
 
@@ -168,19 +176,6 @@ export interface SqlMigrationPlanOperation<TTargetDetails> extends MigrationPlan
168
176
  readonly meta?: AnyRecord;
169
177
  }
170
178
 
171
- /**
172
- * Union of all operation shapes a SQL-family migration may emit: schema-facing
173
- * `SqlMigrationPlanOperation`s and family-agnostic `DataTransformOperation`s.
174
- *
175
- * Mirrors `AnyMongoMigrationOperation` in shape — the runner already handles
176
- * both branches via `isDataTransformOperation`, and authored `migration.ts`
177
- * files must be able to intermix `dataTransform(endContract, …)` calls with
178
- * DDL factory calls (e.g. `setNotNull(…)`) in a single `operations` array.
179
- */
180
- export type AnySqlMigrationOperation<TTargetDetails> =
181
- | SqlMigrationPlanOperation<TTargetDetails>
182
- | DataTransformOperation;
183
-
184
179
  export interface SqlMigrationPlanContractInfo {
185
180
  readonly storageHash: string;
186
181
  readonly profileHash?: string;
@@ -197,6 +192,15 @@ export interface SqlMigrationPlan<TTargetDetails> extends MigrationPlan {
197
192
  */
198
193
  readonly destination: SqlMigrationPlanContractInfo;
199
194
  readonly operations: readonly SqlMigrationPlanOperation<TTargetDetails>[];
195
+ /**
196
+ * Sorted, deduplicated invariant ids declared by this plan's data-transform
197
+ * ops. Required at the SQL-family layer (the SQL runners consume this as
198
+ * the source of truth for marker writes and self-edge no-op checks); the
199
+ * framework-level {@link MigrationPlan.providedInvariants} stays optional
200
+ * because `db init` / `db update` plans don't have a corresponding
201
+ * migration manifest.
202
+ */
203
+ readonly providedInvariants: readonly string[];
200
204
  readonly meta?: AnyRecord;
201
205
  }
202
206
 
@@ -243,14 +247,21 @@ export interface SqlMigrationPlannerPlanOptions {
243
247
  readonly policy: MigrationOperationPolicy;
244
248
  readonly schemaName?: string;
245
249
  /**
246
- * The "from" contract (state the planner assumes the database starts at).
247
- * Only `migration plan` supplies this; `db update` / `db init` reconcile
248
- * against the live schema with no old contract. Strategies that need
249
- * from/to column-shape comparisons (unsafe type change, nullability
250
+ * The "from" contract (state the planner assumes the database starts at),
251
+ * or `null` for reconciliation flows that have no prior contract.
252
+ *
253
+ * Required at every call site so the structural fact "I have a prior
254
+ * contract / I don't" is visible in the type. `migration plan` supplies
255
+ * the previous bundle's `metadata.toContract`; `db update` / `db init`
256
+ * reconcile against the live schema and pass `null`. Strategies that
257
+ * need from/to column-shape comparisons (unsafe type change, nullability
250
258
  * tightening) use this to decide whether to emit `dataTransform`
251
- * placeholders.
259
+ * placeholders; they short-circuit when it is `null`.
260
+ *
261
+ * Planners also derive the "from" identity they stamp onto the produced
262
+ * plan's `describe()` as `fromContract?.storage.storageHash ?? null`.
252
263
  */
253
- readonly fromContract?: Contract<SqlStorage> | null;
264
+ readonly fromContract: Contract<SqlStorage> | null;
254
265
  /**
255
266
  * Active framework components participating in this composition.
256
267
  * SQL targets can interpret this list to derive database dependencies.
@@ -305,6 +316,7 @@ export type SqlMigrationRunnerErrorCode =
305
316
  | 'PRECHECK_FAILED'
306
317
  | 'POSTCHECK_FAILED'
307
318
  | 'SCHEMA_VERIFY_FAILED'
319
+ | 'FOREIGN_KEY_VIOLATION'
308
320
  | 'EXECUTION_FAILED';
309
321
 
310
322
  export interface SqlMigrationRunnerFailure extends MigrationRunnerFailure {
@@ -337,5 +349,11 @@ export interface CreateSqlMigrationPlanOptions<TTargetDetails> {
337
349
  readonly origin?: SqlMigrationPlanContractInfo | null;
338
350
  readonly destination: SqlMigrationPlanContractInfo;
339
351
  readonly operations: readonly SqlMigrationPlanOperation<TTargetDetails>[];
352
+ /**
353
+ * Sorted, deduplicated invariant ids for this plan; mirrors the required
354
+ * field on {@link SqlMigrationPlan}. Callers without a migration manifest
355
+ * (`db init`, `db update`, planner-built plans) pass `[]`.
356
+ */
357
+ readonly providedInvariants: readonly string[];
340
358
  readonly meta?: AnyRecord;
341
359
  }
@@ -0,0 +1,62 @@
1
+ import type {
2
+ MigrationPlanOperation,
3
+ OperationPreview,
4
+ } from '@prisma-next/framework-components/control';
5
+
6
+ /**
7
+ * Shape of an SQL execute step on `SqlMigrationPlanOperation`. Used for runtime
8
+ * type narrowing without importing the concrete SQL type.
9
+ */
10
+ interface SqlExecuteStep {
11
+ readonly sql: string;
12
+ }
13
+
14
+ function isDdlStatement(sqlStatement: string): boolean {
15
+ const trimmed = sqlStatement.trim().toLowerCase();
16
+ return (
17
+ trimmed.startsWith('create ') || trimmed.startsWith('alter ') || trimmed.startsWith('drop ')
18
+ );
19
+ }
20
+
21
+ function hasExecuteSteps(
22
+ operation: MigrationPlanOperation,
23
+ ): operation is MigrationPlanOperation & { readonly execute: readonly SqlExecuteStep[] } {
24
+ const candidate = operation as unknown as Record<string, unknown>;
25
+ if (!('execute' in candidate) || !Array.isArray(candidate['execute'])) {
26
+ return false;
27
+ }
28
+ return candidate['execute'].every(
29
+ (step: unknown) => typeof step === 'object' && step !== null && 'sql' in step,
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Extracts a best-effort SQL DDL preview for CLI plan output.
35
+ * Presentation-only: never used to decide migration correctness.
36
+ */
37
+ export function extractSqlDdl(operations: readonly MigrationPlanOperation[]): string[] {
38
+ const statements: string[] = [];
39
+ for (const operation of operations) {
40
+ if (!hasExecuteSteps(operation)) {
41
+ continue;
42
+ }
43
+ for (const step of operation.execute) {
44
+ if (typeof step.sql === 'string' && isDdlStatement(step.sql)) {
45
+ statements.push(step.sql.trim());
46
+ }
47
+ }
48
+ }
49
+ return statements;
50
+ }
51
+
52
+ /**
53
+ * Wraps `extractSqlDdl` into the family-agnostic `OperationPreview` shape.
54
+ * Each statement carries `language: 'sql'`.
55
+ */
56
+ export function sqlOperationsToPreview(
57
+ operations: readonly MigrationPlanOperation[],
58
+ ): OperationPreview {
59
+ return {
60
+ statements: extractSqlDdl(operations).map((text) => ({ text, language: 'sql' })),
61
+ };
62
+ }
@@ -0,0 +1,56 @@
1
+ import type { ColumnDefault } from '@prisma-next/contract/types';
2
+
3
+ const DEFAULT_FUNCTION_ATTRIBUTES: Readonly<Record<string, string>> = {
4
+ 'autoincrement()': '@default(autoincrement())',
5
+ 'now()': '@default(now())',
6
+ };
7
+
8
+ export interface DefaultMappingOptions {
9
+ readonly functionAttributes?: Readonly<Record<string, string>>;
10
+ readonly fallbackFunctionAttribute?: ((expression: string) => string | undefined) | undefined;
11
+ }
12
+
13
+ export type DefaultMappingResult = { readonly attribute: string } | { readonly comment: string };
14
+
15
+ export function mapDefault(
16
+ columnDefault: ColumnDefault,
17
+ options?: DefaultMappingOptions,
18
+ ): DefaultMappingResult {
19
+ switch (columnDefault.kind) {
20
+ case 'literal':
21
+ return { attribute: `@default(${formatLiteralValue(columnDefault.value)})` };
22
+ case 'function': {
23
+ const attribute =
24
+ options?.functionAttributes?.[columnDefault.expression] ??
25
+ DEFAULT_FUNCTION_ATTRIBUTES[columnDefault.expression] ??
26
+ options?.fallbackFunctionAttribute?.(columnDefault.expression);
27
+ return attribute
28
+ ? { attribute }
29
+ : { comment: `// Raw default: ${columnDefault.expression.replace(/[\r\n]+/g, ' ')}` };
30
+ }
31
+ }
32
+ }
33
+
34
+ function formatLiteralValue(value: unknown): string {
35
+ if (value === null) {
36
+ return 'null';
37
+ }
38
+
39
+ switch (typeof value) {
40
+ case 'boolean':
41
+ case 'number':
42
+ return String(value);
43
+ case 'string':
44
+ return quoteString(value);
45
+ default:
46
+ return quoteString(JSON.stringify(value));
47
+ }
48
+ }
49
+
50
+ function quoteString(str: string): string {
51
+ return `"${escapeString(str)}"`;
52
+ }
53
+
54
+ function escapeString(str: string): string {
55
+ return JSON.stringify(str).slice(1, -1);
56
+ }
@@ -0,0 +1,178 @@
1
+ const PSL_RESERVED_WORDS = new Set(['model', 'enum', 'types', 'type', 'generator', 'datasource']);
2
+
3
+ const IDENTIFIER_PART_PATTERN = /[A-Za-z0-9]+/g;
4
+
5
+ type NameResult = {
6
+ readonly name: string;
7
+ readonly map?: string;
8
+ };
9
+
10
+ function hasSeparators(input: string): boolean {
11
+ return /[^A-Za-z0-9]/.test(input);
12
+ }
13
+
14
+ function extractIdentifierParts(input: string): string[] {
15
+ return input.match(IDENTIFIER_PART_PATTERN) ?? [];
16
+ }
17
+
18
+ function createSyntheticIdentifier(input: string): string {
19
+ let hash = 2166136261;
20
+
21
+ for (const char of input) {
22
+ hash ^= char.codePointAt(0) ?? 0;
23
+ hash = Math.imul(hash, 16777619);
24
+ }
25
+
26
+ return `x${(hash >>> 0).toString(16)}`;
27
+ }
28
+
29
+ function sanitizeIdentifierCharacters(input: string): string {
30
+ const sanitized = input.replace(/[^\w]/g, '');
31
+ return sanitized.length > 0 ? sanitized : createSyntheticIdentifier(input);
32
+ }
33
+
34
+ function capitalize(word: string): string {
35
+ return word.charAt(0).toUpperCase() + word.slice(1);
36
+ }
37
+
38
+ function snakeToPascalCase(input: string): string {
39
+ const parts = extractIdentifierParts(input);
40
+ if (parts.length === 0) {
41
+ return capitalize(sanitizeIdentifierCharacters(input));
42
+ }
43
+ return parts.map(capitalize).join('');
44
+ }
45
+
46
+ function snakeToCamelCase(input: string): string {
47
+ const parts = extractIdentifierParts(input);
48
+ if (parts.length === 0) {
49
+ return sanitizeIdentifierCharacters(input);
50
+ }
51
+ const [firstPart = input, ...rest] = parts;
52
+ return firstPart.charAt(0).toLowerCase() + firstPart.slice(1) + rest.map(capitalize).join('');
53
+ }
54
+
55
+ function needsEscaping(name: string): boolean {
56
+ return PSL_RESERVED_WORDS.has(name.toLowerCase()) || /^\d/.test(name);
57
+ }
58
+
59
+ function escapeName(name: string): string {
60
+ return `_${name}`;
61
+ }
62
+
63
+ function escapeIfNeeded(name: string): string {
64
+ return needsEscaping(name) ? escapeName(name) : name;
65
+ }
66
+
67
+ export function toModelName(tableName: string): NameResult {
68
+ let name: string;
69
+
70
+ if (hasSeparators(tableName)) {
71
+ name = snakeToPascalCase(tableName);
72
+ } else {
73
+ name = tableName.charAt(0).toUpperCase() + tableName.slice(1);
74
+ }
75
+
76
+ if (needsEscaping(name)) {
77
+ const escaped = escapeName(name);
78
+ return { name: escaped, map: tableName };
79
+ }
80
+
81
+ if (name !== tableName) {
82
+ return { name, map: tableName };
83
+ }
84
+
85
+ return { name };
86
+ }
87
+
88
+ export function toFieldName(columnName: string): NameResult {
89
+ let name: string;
90
+
91
+ if (hasSeparators(columnName)) {
92
+ name = snakeToCamelCase(columnName);
93
+ } else {
94
+ name = columnName.charAt(0).toLowerCase() + columnName.slice(1);
95
+ }
96
+
97
+ if (needsEscaping(name)) {
98
+ const escaped = escapeName(name);
99
+ return { name: escaped, map: columnName };
100
+ }
101
+
102
+ if (name !== columnName) {
103
+ return { name, map: columnName };
104
+ }
105
+
106
+ return { name };
107
+ }
108
+
109
+ export function toEnumName(pgTypeName: string): NameResult {
110
+ let name: string;
111
+
112
+ if (hasSeparators(pgTypeName)) {
113
+ name = snakeToPascalCase(pgTypeName);
114
+ } else {
115
+ name = pgTypeName.charAt(0).toUpperCase() + pgTypeName.slice(1);
116
+ }
117
+
118
+ if (needsEscaping(name)) {
119
+ const escaped = escapeName(name);
120
+ return { name: escaped, map: pgTypeName };
121
+ }
122
+
123
+ if (name !== pgTypeName) {
124
+ return { name, map: pgTypeName };
125
+ }
126
+
127
+ return { name };
128
+ }
129
+
130
+ export function pluralize(word: string): string {
131
+ if (
132
+ word.endsWith('s') ||
133
+ word.endsWith('x') ||
134
+ word.endsWith('z') ||
135
+ word.endsWith('ch') ||
136
+ word.endsWith('sh')
137
+ ) {
138
+ return `${word}es`;
139
+ }
140
+ if (word.endsWith('y') && !/[aeiou]y$/i.test(word)) {
141
+ return `${word.slice(0, -1)}ies`;
142
+ }
143
+ return `${word}s`;
144
+ }
145
+
146
+ export function deriveRelationFieldName(
147
+ fkColumns: readonly string[],
148
+ referencedTableName: string,
149
+ ): string {
150
+ if (fkColumns.length === 1) {
151
+ const [col = referencedTableName] = fkColumns;
152
+ const stripped = col.replace(/_id$/i, '').replace(/Id$/, '');
153
+
154
+ if (stripped.length > 0 && stripped !== col) {
155
+ return escapeIfNeeded(snakeToCamelCase(stripped));
156
+ }
157
+ return escapeIfNeeded(snakeToCamelCase(referencedTableName));
158
+ }
159
+
160
+ return escapeIfNeeded(snakeToCamelCase(referencedTableName));
161
+ }
162
+
163
+ export function deriveBackRelationFieldName(childModelName: string, isOneToOne: boolean): string {
164
+ const base = childModelName.charAt(0).toLowerCase() + childModelName.slice(1);
165
+ return isOneToOne ? base : pluralize(base);
166
+ }
167
+
168
+ export function toNamedTypeName(columnName: string): string {
169
+ let name: string;
170
+
171
+ if (hasSeparators(columnName)) {
172
+ name = snakeToPascalCase(columnName);
173
+ } else {
174
+ name = columnName.charAt(0).toUpperCase() + columnName.slice(1);
175
+ }
176
+
177
+ return escapeIfNeeded(name);
178
+ }