@prisma-next/family-sql 0.5.0-dev.4 → 0.5.0-dev.41

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 (42) hide show
  1. package/README.md +2 -2
  2. package/dist/control-adapter.d.mts +12 -1
  3. package/dist/control-adapter.d.mts.map +1 -1
  4. package/dist/control.d.mts +3 -2
  5. package/dist/control.d.mts.map +1 -1
  6. package/dist/control.mjs +1025 -11
  7. package/dist/control.mjs.map +1 -1
  8. package/dist/migration.d.mts +14 -2
  9. package/dist/migration.d.mts.map +1 -1
  10. package/dist/migration.mjs +16 -1
  11. package/dist/migration.mjs.map +1 -1
  12. package/dist/schema-verify.d.mts +2 -2
  13. package/dist/{types-C6K4mxDM.d.mts → types-gLyIyd2X.d.mts} +36 -11
  14. package/dist/types-gLyIyd2X.d.mts.map +1 -0
  15. package/dist/verify-BdES8wgQ.mjs +82 -0
  16. package/dist/verify-BdES8wgQ.mjs.map +1 -0
  17. package/dist/verify-sql-schema-Ovz7RXR5.mjs.map +1 -1
  18. package/dist/{verify-sql-schema-BBhkqEDo.d.mts → verify-sql-schema-_EoNcGIq.d.mts} +2 -2
  19. package/dist/{verify-sql-schema-BBhkqEDo.d.mts.map → verify-sql-schema-_EoNcGIq.d.mts.map} +1 -1
  20. package/dist/verify.d.mts +16 -20
  21. package/dist/verify.d.mts.map +1 -1
  22. package/dist/verify.mjs +2 -2
  23. package/package.json +19 -19
  24. package/src/core/control-adapter.ts +12 -0
  25. package/src/core/control-instance.ts +43 -15
  26. package/src/core/migrations/plan-helpers.ts +1 -0
  27. package/src/core/migrations/types.ts +29 -6
  28. package/src/core/operation-preview.ts +62 -0
  29. package/src/core/psl-contract-infer/default-mapping.ts +56 -0
  30. package/src/core/psl-contract-infer/name-transforms.ts +178 -0
  31. package/src/core/psl-contract-infer/postgres-default-mapping.ts +16 -0
  32. package/src/core/psl-contract-infer/postgres-type-map.ts +165 -0
  33. package/src/core/psl-contract-infer/printer-config.ts +55 -0
  34. package/src/core/psl-contract-infer/raw-default-parser.ts +91 -0
  35. package/src/core/psl-contract-infer/relation-inference.ts +196 -0
  36. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +832 -0
  37. package/src/core/sql-migration.ts +16 -1
  38. package/src/core/verify.ts +46 -108
  39. package/src/exports/verify.ts +1 -1
  40. package/dist/types-C6K4mxDM.d.mts.map +0 -1
  41. package/dist/verify-4GshvY4p.mjs +0 -122
  42. package/dist/verify-4GshvY4p.mjs.map +0 -1
package/package.json CHANGED
@@ -1,35 +1,35 @@
1
1
  {
2
2
  "name": "@prisma-next/family-sql",
3
- "version": "0.5.0-dev.4",
3
+ "version": "0.5.0-dev.41",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "SQL family descriptor for Prisma Next",
7
7
  "dependencies": {
8
8
  "arktype": "^2.0.0",
9
- "@prisma-next/contract": "0.5.0-dev.4",
10
- "@prisma-next/emitter": "0.5.0-dev.4",
11
- "@prisma-next/operations": "0.5.0-dev.4",
12
- "@prisma-next/migration-tools": "0.5.0-dev.4",
13
- "@prisma-next/framework-components": "0.5.0-dev.4",
14
- "@prisma-next/runtime-executor": "0.5.0-dev.4",
15
- "@prisma-next/sql-contract-emitter": "0.5.0-dev.4",
16
- "@prisma-next/sql-contract": "0.5.0-dev.4",
17
- "@prisma-next/sql-contract-ts": "0.5.0-dev.4",
18
- "@prisma-next/sql-runtime": "0.5.0-dev.4",
19
- "@prisma-next/sql-operations": "0.5.0-dev.4",
20
- "@prisma-next/sql-relational-core": "0.5.0-dev.4",
21
- "@prisma-next/sql-schema-ir": "0.5.0-dev.4",
22
- "@prisma-next/utils": "0.5.0-dev.4"
9
+ "@prisma-next/contract": "0.5.0-dev.41",
10
+ "@prisma-next/framework-components": "0.5.0-dev.41",
11
+ "@prisma-next/emitter": "0.5.0-dev.41",
12
+ "@prisma-next/migration-tools": "0.5.0-dev.41",
13
+ "@prisma-next/operations": "0.5.0-dev.41",
14
+ "@prisma-next/sql-contract": "0.5.0-dev.41",
15
+ "@prisma-next/sql-contract-ts": "0.5.0-dev.41",
16
+ "@prisma-next/sql-operations": "0.5.0-dev.41",
17
+ "@prisma-next/sql-runtime": "0.5.0-dev.41",
18
+ "@prisma-next/sql-relational-core": "0.5.0-dev.41",
19
+ "@prisma-next/sql-contract-emitter": "0.5.0-dev.41",
20
+ "@prisma-next/sql-schema-ir": "0.5.0-dev.41",
21
+ "@prisma-next/utils": "0.5.0-dev.41"
23
22
  },
24
23
  "devDependencies": {
25
24
  "tsdown": "0.18.4",
26
25
  "typescript": "5.9.3",
27
26
  "vitest": "4.0.17",
28
- "@prisma-next/driver-postgres": "0.5.0-dev.4",
29
- "@prisma-next/sql-contract-psl": "0.5.0-dev.4",
30
- "@prisma-next/psl-parser": "0.5.0-dev.4",
31
- "@prisma-next/tsconfig": "0.0.0",
27
+ "@prisma-next/driver-postgres": "0.5.0-dev.41",
28
+ "@prisma-next/psl-parser": "0.5.0-dev.41",
29
+ "@prisma-next/psl-printer": "0.5.0-dev.41",
32
30
  "@prisma-next/test-utils": "0.0.1",
31
+ "@prisma-next/tsconfig": "0.0.0",
32
+ "@prisma-next/sql-contract-psl": "0.5.0-dev.41",
33
33
  "@prisma-next/tsdown": "0.0.0"
34
34
  },
35
35
  "files": [
@@ -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 {
@@ -101,6 +101,7 @@ export function createMigrationPlan<TTargetDetails>(
101
101
  : {}),
102
102
  destination: Object.freeze({ ...options.destination }),
103
103
  operations: freezeOperations(options.operations),
104
+ providedInvariants: Object.freeze([...options.providedInvariants]),
104
105
  ...(options.meta ? { meta: cloneRecord(options.meta) } : {}),
105
106
  });
106
107
  }
@@ -197,6 +197,15 @@ export interface SqlMigrationPlan<TTargetDetails> extends MigrationPlan {
197
197
  */
198
198
  readonly destination: SqlMigrationPlanContractInfo;
199
199
  readonly operations: readonly SqlMigrationPlanOperation<TTargetDetails>[];
200
+ /**
201
+ * Sorted, deduplicated invariant ids declared by this plan's data-transform
202
+ * ops. Required at the SQL-family layer (the SQL runners consume this as
203
+ * the source of truth for marker writes and self-edge no-op checks); the
204
+ * framework-level {@link MigrationPlan.providedInvariants} stays optional
205
+ * because `db init` / `db update` plans don't have a corresponding
206
+ * migration manifest.
207
+ */
208
+ readonly providedInvariants: readonly string[];
200
209
  readonly meta?: AnyRecord;
201
210
  }
202
211
 
@@ -243,14 +252,21 @@ export interface SqlMigrationPlannerPlanOptions {
243
252
  readonly policy: MigrationOperationPolicy;
244
253
  readonly schemaName?: string;
245
254
  /**
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
255
+ * The "from" contract (state the planner assumes the database starts at),
256
+ * or `null` for reconciliation flows that have no prior contract.
257
+ *
258
+ * Required at every call site so the structural fact "I have a prior
259
+ * contract / I don't" is visible in the type. `migration plan` supplies
260
+ * the previous bundle's `metadata.toContract`; `db update` / `db init`
261
+ * reconcile against the live schema and pass `null`. Strategies that
262
+ * need from/to column-shape comparisons (unsafe type change, nullability
250
263
  * tightening) use this to decide whether to emit `dataTransform`
251
- * placeholders.
264
+ * placeholders; they short-circuit when it is `null`.
265
+ *
266
+ * Planners also derive the "from" identity they stamp onto the produced
267
+ * plan's `describe()` as `fromContract?.storage.storageHash ?? null`.
252
268
  */
253
- readonly fromContract?: Contract<SqlStorage> | null;
269
+ readonly fromContract: Contract<SqlStorage> | null;
254
270
  /**
255
271
  * Active framework components participating in this composition.
256
272
  * SQL targets can interpret this list to derive database dependencies.
@@ -305,6 +321,7 @@ export type SqlMigrationRunnerErrorCode =
305
321
  | 'PRECHECK_FAILED'
306
322
  | 'POSTCHECK_FAILED'
307
323
  | 'SCHEMA_VERIFY_FAILED'
324
+ | 'FOREIGN_KEY_VIOLATION'
308
325
  | 'EXECUTION_FAILED';
309
326
 
310
327
  export interface SqlMigrationRunnerFailure extends MigrationRunnerFailure {
@@ -337,5 +354,11 @@ export interface CreateSqlMigrationPlanOptions<TTargetDetails> {
337
354
  readonly origin?: SqlMigrationPlanContractInfo | null;
338
355
  readonly destination: SqlMigrationPlanContractInfo;
339
356
  readonly operations: readonly SqlMigrationPlanOperation<TTargetDetails>[];
357
+ /**
358
+ * Sorted, deduplicated invariant ids for this plan; mirrors the required
359
+ * field on {@link SqlMigrationPlan}. Callers without a migration manifest
360
+ * (`db init`, `db update`, planner-built plans) pass `[]`.
361
+ */
362
+ readonly providedInvariants: readonly string[];
340
363
  readonly meta?: AnyRecord;
341
364
  }
@@ -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
+ }
@@ -0,0 +1,16 @@
1
+ import type { DefaultMappingOptions } from './default-mapping';
2
+
3
+ const POSTGRES_FUNCTION_ATTRIBUTES: Readonly<Record<string, string>> = {
4
+ 'gen_random_uuid()': '@default(dbgenerated("gen_random_uuid()"))',
5
+ };
6
+
7
+ function formatDbGeneratedAttribute(expression: string): string {
8
+ return `@default(dbgenerated(${JSON.stringify(expression)}))`;
9
+ }
10
+
11
+ export function createPostgresDefaultMapping(): DefaultMappingOptions {
12
+ return {
13
+ functionAttributes: POSTGRES_FUNCTION_ATTRIBUTES,
14
+ fallbackFunctionAttribute: formatDbGeneratedAttribute,
15
+ };
16
+ }