@prisma-next/family-sql 0.5.0-dev.3 → 0.5.0-dev.30

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 (36) 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 +2 -2
  5. package/dist/control.mjs +1024 -11
  6. package/dist/control.mjs.map +1 -1
  7. package/dist/migration.d.mts +1 -1
  8. package/dist/schema-verify.d.mts +2 -2
  9. package/dist/{types-C6K4mxDM.d.mts → types-sZihdnGx.d.mts} +14 -5
  10. package/dist/types-sZihdnGx.d.mts.map +1 -0
  11. package/dist/verify-BdES8wgQ.mjs +82 -0
  12. package/dist/verify-BdES8wgQ.mjs.map +1 -0
  13. package/dist/verify-sql-schema-Ovz7RXR5.mjs.map +1 -1
  14. package/dist/{verify-sql-schema-BBhkqEDo.d.mts → verify-sql-schema-_EoNcGIq.d.mts} +2 -2
  15. package/dist/{verify-sql-schema-BBhkqEDo.d.mts.map → verify-sql-schema-_EoNcGIq.d.mts.map} +1 -1
  16. package/dist/verify.d.mts +16 -20
  17. package/dist/verify.d.mts.map +1 -1
  18. package/dist/verify.mjs +2 -2
  19. package/package.json +18 -18
  20. package/src/core/control-adapter.ts +12 -0
  21. package/src/core/control-instance.ts +43 -15
  22. package/src/core/migrations/types.ts +7 -0
  23. package/src/core/operation-preview.ts +62 -0
  24. package/src/core/psl-contract-infer/default-mapping.ts +56 -0
  25. package/src/core/psl-contract-infer/name-transforms.ts +178 -0
  26. package/src/core/psl-contract-infer/postgres-default-mapping.ts +16 -0
  27. package/src/core/psl-contract-infer/postgres-type-map.ts +165 -0
  28. package/src/core/psl-contract-infer/printer-config.ts +55 -0
  29. package/src/core/psl-contract-infer/raw-default-parser.ts +91 -0
  30. package/src/core/psl-contract-infer/relation-inference.ts +196 -0
  31. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +832 -0
  32. package/src/core/verify.ts +46 -108
  33. package/src/exports/verify.ts +1 -1
  34. package/dist/types-C6K4mxDM.d.mts.map +0 -1
  35. package/dist/verify-4GshvY4p.mjs +0 -122
  36. package/dist/verify-4GshvY4p.mjs.map +0 -1
package/dist/verify.d.mts CHANGED
@@ -1,31 +1,27 @@
1
- import { ControlDriverInstance } from "@prisma-next/framework-components/control";
2
1
  import { ContractMarkerRecord } from "@prisma-next/contract/types";
3
2
 
4
3
  //#region src/core/verify.d.ts
5
4
 
6
5
  /**
7
- * Parses a contract marker row from database query result.
8
- * This is SQL-specific parsing logic (handles SQL row structure with snake_case columns).
9
- */
10
- declare function parseContractMarkerRow(row: unknown): ContractMarkerRecord;
11
- /**
12
- * Returns the SQL statement to read the contract marker.
13
- * This is a migration-plane helper (no runtime imports).
14
- * @internal - Used internally by readMarker(). Prefer readMarker() for Control Plane usage.
6
+ * Wire shape of a `prisma_contract.marker` row as it comes out of a SQL
7
+ * driver. Snake-cased to match the on-disk column names. Shared by every
8
+ * SQL target's `readMarker` so each runner doesn't redeclare it inline.
15
9
  */
16
- declare function readMarkerSql(): {
17
- readonly sql: string;
18
- readonly params: readonly unknown[];
10
+ type ContractMarkerRow = {
11
+ core_hash: string;
12
+ profile_hash: string;
13
+ contract_json: unknown | null;
14
+ canonical_version: number | null;
15
+ updated_at: Date | string;
16
+ app_tag: string | null;
17
+ meta: unknown | null;
18
+ invariants: unknown;
19
19
  };
20
20
  /**
21
- * Reads the contract marker from the database using the provided driver.
22
- * Returns the parsed marker record or null if no marker is found.
23
- * This abstracts SQL-specific details from the Control Plane.
24
- *
25
- * @param driver - ControlDriverInstance instance for executing queries
26
- * @returns Promise resolving to ContractMarkerRecord or null if marker not found
21
+ * Parses a contract marker row from database query result.
22
+ * This is SQL-specific parsing logic (handles SQL row structure with snake_case columns).
27
23
  */
28
- declare function readMarker(driver: ControlDriverInstance<'sql', string>): Promise<ContractMarkerRecord | null>;
24
+ declare function parseContractMarkerRow(row: unknown): ContractMarkerRecord;
29
25
  //#endregion
30
- export { parseContractMarkerRow, readMarker, readMarkerSql };
26
+ export { type ContractMarkerRow, parseContractMarkerRow };
31
27
  //# sourceMappingURL=verify.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.mts","names":[],"sources":["../src/core/verify.ts"],"sourcesContent":[],"mappings":";;;;;;;AA4CA;AAuCA;AA6CsB,iBApFN,sBAAA,CAoFgB,GAAA,EAAA,OAAA,CAAA,EApFsB,oBAoFtB;;;;;;iBA7ChB,aAAA,CAAA;;;;;;;;;;;;iBA6CM,UAAA,SACZ,uCACP,QAAQ"}
1
+ {"version":3,"file":"verify.d.mts","names":[],"sources":["../src/core/verify.ts"],"sourcesContent":[],"mappings":";;;;;;AAiDA;AA6BA;;KA7BY,iBAAA;;;;;cAKE;;;;;;;;;iBAwBE,sBAAA,gBAAsC"}
package/dist/verify.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { i as readMarkerSql, n as parseContractMarkerRow, r as readMarker } from "./verify-4GshvY4p.mjs";
1
+ import { n as parseContractMarkerRow } from "./verify-BdES8wgQ.mjs";
2
2
 
3
- export { parseContractMarkerRow, readMarker, readMarkerSql };
3
+ export { parseContractMarkerRow };
package/package.json CHANGED
@@ -1,34 +1,34 @@
1
1
  {
2
2
  "name": "@prisma-next/family-sql",
3
- "version": "0.5.0-dev.3",
3
+ "version": "0.5.0-dev.30",
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.3",
10
- "@prisma-next/emitter": "0.5.0-dev.3",
11
- "@prisma-next/framework-components": "0.5.0-dev.3",
12
- "@prisma-next/migration-tools": "0.5.0-dev.3",
13
- "@prisma-next/operations": "0.5.0-dev.3",
14
- "@prisma-next/sql-contract": "0.5.0-dev.3",
15
- "@prisma-next/runtime-executor": "0.5.0-dev.3",
16
- "@prisma-next/sql-contract-emitter": "0.5.0-dev.3",
17
- "@prisma-next/sql-contract-ts": "0.5.0-dev.3",
18
- "@prisma-next/sql-operations": "0.5.0-dev.3",
19
- "@prisma-next/sql-relational-core": "0.5.0-dev.3",
20
- "@prisma-next/sql-runtime": "0.5.0-dev.3",
21
- "@prisma-next/sql-schema-ir": "0.5.0-dev.3",
22
- "@prisma-next/utils": "0.5.0-dev.3"
9
+ "@prisma-next/contract": "0.5.0-dev.30",
10
+ "@prisma-next/emitter": "0.5.0-dev.30",
11
+ "@prisma-next/framework-components": "0.5.0-dev.30",
12
+ "@prisma-next/migration-tools": "0.5.0-dev.30",
13
+ "@prisma-next/sql-contract-ts": "0.5.0-dev.30",
14
+ "@prisma-next/sql-contract-emitter": "0.5.0-dev.30",
15
+ "@prisma-next/operations": "0.5.0-dev.30",
16
+ "@prisma-next/sql-contract": "0.5.0-dev.30",
17
+ "@prisma-next/sql-operations": "0.5.0-dev.30",
18
+ "@prisma-next/sql-runtime": "0.5.0-dev.30",
19
+ "@prisma-next/sql-relational-core": "0.5.0-dev.30",
20
+ "@prisma-next/sql-schema-ir": "0.5.0-dev.30",
21
+ "@prisma-next/utils": "0.5.0-dev.30"
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.3",
29
- "@prisma-next/sql-contract-psl": "0.5.0-dev.3",
27
+ "@prisma-next/driver-postgres": "0.5.0-dev.30",
28
+ "@prisma-next/sql-contract-psl": "0.5.0-dev.30",
30
29
  "@prisma-next/test-utils": "0.0.1",
31
- "@prisma-next/psl-parser": "0.5.0-dev.3",
30
+ "@prisma-next/psl-printer": "0.5.0-dev.30",
31
+ "@prisma-next/psl-parser": "0.5.0-dev.30",
32
32
  "@prisma-next/tsconfig": "0.0.0",
33
33
  "@prisma-next/tsdown": "0.0.0"
34
34
  },
@@ -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 {
@@ -296,6 +296,12 @@ export interface SqlMigrationRunnerExecuteOptions<TTargetDetails> {
296
296
  * All components must have matching familyId ('sql') and targetId.
297
297
  */
298
298
  readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
299
+ /**
300
+ * Invariant ids contributed by this apply (the migration's `providedInvariants`).
301
+ * The runner unions these into `marker.invariants` atomically with the marker write.
302
+ * Defaults to `[]` for marker-only flows (`db update`, `db init`).
303
+ */
304
+ readonly invariants?: readonly string[];
299
305
  }
300
306
 
301
307
  export type SqlMigrationRunnerErrorCode =
@@ -305,6 +311,7 @@ export type SqlMigrationRunnerErrorCode =
305
311
  | 'PRECHECK_FAILED'
306
312
  | 'POSTCHECK_FAILED'
307
313
  | 'SCHEMA_VERIFY_FAILED'
314
+ | 'FOREIGN_KEY_VIOLATION'
308
315
  | 'EXECUTION_FAILED';
309
316
 
310
317
  export interface SqlMigrationRunnerFailure extends MigrationRunnerFailure {
@@ -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
+ }