@prisma-next/family-sql 0.5.0-dev.26 → 0.5.0-dev.28

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.
@@ -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,6 +42,8 @@ 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
48
  import { collectSupportedCodecTypeIds } from './verify';
42
49
 
@@ -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;
@@ -573,6 +586,14 @@ export function createSqlFamilyInstance<TTargetId extends string>(
573
586
  return getControlAdapter().introspect(options.driver, options.contract);
574
587
  },
575
588
 
589
+ inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst {
590
+ return sqlSchemaIrToPslAst(schemaIR);
591
+ },
592
+
593
+ toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview {
594
+ return sqlOperationsToPreview(operations);
595
+ },
596
+
576
597
  toSchemaView(schema: SqlSchemaIR): CoreSchemaView {
577
598
  const tableNodes: readonly SchemaTreeNode[] = Object.entries(schema.tables).map(
578
599
  ([tableName, table]: [string, SqlTableIR]) => {
@@ -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
+ }
@@ -0,0 +1,165 @@
1
+ import type {
2
+ EnumInfo,
3
+ PslNativeTypeAttribute,
4
+ PslTypeMap,
5
+ PslTypeResolution,
6
+ } from './printer-config';
7
+
8
+ const POSTGRES_TO_PSL: Record<string, string> = {
9
+ text: 'String',
10
+ bool: 'Boolean',
11
+ boolean: 'Boolean',
12
+ int4: 'Int',
13
+ integer: 'Int',
14
+ int8: 'BigInt',
15
+ bigint: 'BigInt',
16
+ float8: 'Float',
17
+ 'double precision': 'Float',
18
+ numeric: 'Decimal',
19
+ decimal: 'Decimal',
20
+ timestamptz: 'DateTime',
21
+ 'timestamp with time zone': 'DateTime',
22
+ jsonb: 'Json',
23
+ bytea: 'Bytes',
24
+ };
25
+
26
+ const PRESERVED_NATIVE_TYPES: Record<
27
+ string,
28
+ { readonly pslType: string; readonly attributeName: string }
29
+ > = {
30
+ 'character varying': { pslType: 'String', attributeName: 'db.VarChar' },
31
+ character: { pslType: 'String', attributeName: 'db.Char' },
32
+ char: { pslType: 'String', attributeName: 'db.Char' },
33
+ varchar: { pslType: 'String', attributeName: 'db.VarChar' },
34
+ uuid: { pslType: 'String', attributeName: 'db.Uuid' },
35
+ int2: { pslType: 'Int', attributeName: 'db.SmallInt' },
36
+ smallint: { pslType: 'Int', attributeName: 'db.SmallInt' },
37
+ float4: { pslType: 'Float', attributeName: 'db.Real' },
38
+ real: { pslType: 'Float', attributeName: 'db.Real' },
39
+ timestamp: { pslType: 'DateTime', attributeName: 'db.Timestamp' },
40
+ 'timestamp without time zone': { pslType: 'DateTime', attributeName: 'db.Timestamp' },
41
+ date: { pslType: 'DateTime', attributeName: 'db.Date' },
42
+ time: { pslType: 'DateTime', attributeName: 'db.Time' },
43
+ 'time without time zone': { pslType: 'DateTime', attributeName: 'db.Time' },
44
+ timetz: { pslType: 'DateTime', attributeName: 'db.Timetz' },
45
+ 'time with time zone': { pslType: 'DateTime', attributeName: 'db.Timetz' },
46
+ json: { pslType: 'Json', attributeName: 'db.Json' },
47
+ };
48
+
49
+ const PARAMETERIZED_NATIVE_TYPES: Record<
50
+ string,
51
+ { readonly pslType: string; readonly attributeName: string }
52
+ > = {
53
+ 'character varying': { pslType: 'String', attributeName: 'db.VarChar' },
54
+ character: { pslType: 'String', attributeName: 'db.Char' },
55
+ char: { pslType: 'String', attributeName: 'db.Char' },
56
+ varchar: { pslType: 'String', attributeName: 'db.VarChar' },
57
+ numeric: { pslType: 'Decimal', attributeName: 'db.Numeric' },
58
+ timestamp: { pslType: 'DateTime', attributeName: 'db.Timestamp' },
59
+ timestamptz: { pslType: 'DateTime', attributeName: 'db.Timestamptz' },
60
+ time: { pslType: 'DateTime', attributeName: 'db.Time' },
61
+ timetz: { pslType: 'DateTime', attributeName: 'db.Timetz' },
62
+ };
63
+
64
+ const PARAMETERIZED_TYPE_PATTERN = /^(.+?)\((.+)\)$/;
65
+
66
+ const ENUM_CODEC_ID = 'pg/enum@1';
67
+
68
+ function getOwnMappingValue(map: Record<string, string>, key: string): string | undefined {
69
+ return Object.hasOwn(map, key) ? map[key] : undefined;
70
+ }
71
+
72
+ function getOwnRecordValue<T>(map: Record<string, T>, key: string): T | undefined {
73
+ return Object.hasOwn(map, key) ? map[key] : undefined;
74
+ }
75
+
76
+ function createNativeTypeAttribute(name: string, args?: readonly string[]): PslNativeTypeAttribute {
77
+ return args && args.length > 0 ? { name, args } : { name };
78
+ }
79
+
80
+ function splitTypeParameterList(params: string): readonly string[] {
81
+ return params
82
+ .split(',')
83
+ .map((part) => part.trim())
84
+ .filter((part) => part.length > 0);
85
+ }
86
+
87
+ export function createPostgresTypeMap(enumTypeNames?: ReadonlySet<string>): PslTypeMap {
88
+ return {
89
+ resolve(nativeType: string): PslTypeResolution {
90
+ if (enumTypeNames?.has(nativeType)) {
91
+ return { pslType: nativeType, nativeType };
92
+ }
93
+
94
+ const paramMatch = nativeType.match(PARAMETERIZED_TYPE_PATTERN);
95
+ if (paramMatch) {
96
+ const [, baseType = nativeType, params = ''] = paramMatch;
97
+ const template = getOwnRecordValue(PARAMETERIZED_NATIVE_TYPES, baseType);
98
+ if (template) {
99
+ return {
100
+ pslType: template.pslType,
101
+ nativeType,
102
+ typeParams: { baseType, params },
103
+ nativeTypeAttribute: createNativeTypeAttribute(
104
+ template.attributeName,
105
+ splitTypeParameterList(params),
106
+ ),
107
+ };
108
+ }
109
+ }
110
+
111
+ const preservedType = getOwnRecordValue(PRESERVED_NATIVE_TYPES, nativeType);
112
+ if (preservedType) {
113
+ return {
114
+ pslType: preservedType.pslType,
115
+ nativeType,
116
+ nativeTypeAttribute: createNativeTypeAttribute(preservedType.attributeName),
117
+ };
118
+ }
119
+
120
+ const pslType = getOwnMappingValue(POSTGRES_TO_PSL, nativeType);
121
+ if (pslType) {
122
+ return {
123
+ pslType,
124
+ nativeType,
125
+ };
126
+ }
127
+
128
+ return { unsupported: true, nativeType };
129
+ },
130
+ };
131
+ }
132
+
133
+ export function extractEnumInfo(annotations?: Record<string, unknown>): EnumInfo {
134
+ const pgAnnotations = annotations?.['pg'] as Record<string, unknown> | undefined;
135
+ const storageTypes = pgAnnotations?.['storageTypes'] as
136
+ | Record<string, { codecId: string; nativeType: string; typeParams?: Record<string, unknown> }>
137
+ | undefined;
138
+
139
+ const typeNames = new Set<string>();
140
+ const definitions = new Map<string, readonly string[]>();
141
+
142
+ if (storageTypes) {
143
+ for (const [key, typeInstance] of Object.entries(storageTypes)) {
144
+ if (typeInstance.codecId === ENUM_CODEC_ID) {
145
+ typeNames.add(key);
146
+ const values = typeInstance.typeParams?.['values'];
147
+ if (Array.isArray(values)) {
148
+ definitions.set(key, values as string[]);
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ return { typeNames, definitions };
155
+ }
156
+
157
+ export function extractEnumTypeNames(annotations?: Record<string, unknown>): ReadonlySet<string> {
158
+ return extractEnumInfo(annotations).typeNames;
159
+ }
160
+
161
+ export function extractEnumDefinitions(
162
+ annotations?: Record<string, unknown>,
163
+ ): ReadonlyMap<string, readonly string[]> {
164
+ return extractEnumInfo(annotations).definitions;
165
+ }
@@ -0,0 +1,55 @@
1
+ import type { ColumnDefault } from '@prisma-next/contract/types';
2
+ import type { DefaultMappingOptions } from './default-mapping';
3
+
4
+ /**
5
+ * Internal printer-shaped configuration, used by the SQL family's
6
+ * `sqlSchemaIrToPslAst` helper (M2). The framework-level psl-printer no longer
7
+ * exposes these — they're consumed only inside the SQL family.
8
+ */
9
+
10
+ export type PslNativeTypeAttribute = {
11
+ readonly name: string;
12
+ readonly args?: readonly string[];
13
+ };
14
+
15
+ export type PslTypeResolution =
16
+ | {
17
+ readonly pslType: string;
18
+ readonly nativeType: string;
19
+ readonly typeParams?: Record<string, unknown>;
20
+ readonly nativeTypeAttribute?: PslNativeTypeAttribute;
21
+ }
22
+ | {
23
+ readonly unsupported: true;
24
+ readonly nativeType: string;
25
+ };
26
+
27
+ export interface PslTypeMap {
28
+ resolve(nativeType: string, annotations?: Record<string, unknown>): PslTypeResolution;
29
+ }
30
+
31
+ export interface EnumInfo {
32
+ readonly typeNames: ReadonlySet<string>;
33
+ readonly definitions: ReadonlyMap<string, readonly string[]>;
34
+ }
35
+
36
+ export interface PslPrinterOptions {
37
+ readonly typeMap: PslTypeMap;
38
+ readonly defaultMapping?: DefaultMappingOptions;
39
+ readonly enumInfo?: EnumInfo;
40
+ readonly parseRawDefault?: (rawDefault: string, nativeType?: string) => ColumnDefault | undefined;
41
+ }
42
+
43
+ export type RelationField = {
44
+ readonly fieldName: string;
45
+ readonly typeName: string;
46
+ readonly referencedTableName?: string | undefined;
47
+ readonly optional: boolean;
48
+ readonly list: boolean;
49
+ readonly relationName?: string | undefined;
50
+ readonly fkName?: string | undefined;
51
+ readonly fields?: readonly string[] | undefined;
52
+ readonly references?: readonly string[] | undefined;
53
+ readonly onDelete?: string | undefined;
54
+ readonly onUpdate?: string | undefined;
55
+ };
@@ -0,0 +1,91 @@
1
+ import type { ColumnDefault } from '@prisma-next/contract/types';
2
+
3
+ const NEXTVAL_PATTERN = /^nextval\s*\(/i;
4
+ const NOW_FUNCTION_PATTERN = /^(now\s*\(\s*\)|CURRENT_TIMESTAMP)$/i;
5
+ const CLOCK_TIMESTAMP_PATTERN = /^clock_timestamp\s*\(\s*\)$/i;
6
+ const TIMESTAMP_CAST_SUFFIX = /::timestamp(?:tz|\s+(?:with|without)\s+time\s+zone)?$/i;
7
+ const TEXT_CAST_SUFFIX = /::text$/i;
8
+ const NOW_LITERAL_PATTERN = /^'now'$/i;
9
+ const UUID_PATTERN = /^gen_random_uuid\s*\(\s*\)$/i;
10
+ const UUID_OSSP_PATTERN = /^uuid_generate_v4\s*\(\s*\)$/i;
11
+ const NULL_PATTERN = /^NULL(?:::.+)?$/i;
12
+ const TRUE_PATTERN = /^true$/i;
13
+ const FALSE_PATTERN = /^false$/i;
14
+ const NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
15
+ const JSON_CAST_SUFFIX = /::jsonb?$/i;
16
+ const STRING_LITERAL_PATTERN = /^'((?:[^']|'')*)'(?:::(?:"[^"]+"|[\w\s]+)(?:\(\d+\))?)?$/;
17
+
18
+ function canonicalizeTimestampDefault(expr: string): string | undefined {
19
+ if (NOW_FUNCTION_PATTERN.test(expr)) return 'now()';
20
+ if (CLOCK_TIMESTAMP_PATTERN.test(expr)) return 'clock_timestamp()';
21
+
22
+ if (!TIMESTAMP_CAST_SUFFIX.test(expr)) return undefined;
23
+
24
+ let inner = expr.replace(TIMESTAMP_CAST_SUFFIX, '').trim();
25
+ if (inner.startsWith('(') && inner.endsWith(')')) {
26
+ inner = inner.slice(1, -1).trim();
27
+ }
28
+
29
+ if (NOW_FUNCTION_PATTERN.test(inner)) return 'now()';
30
+ if (CLOCK_TIMESTAMP_PATTERN.test(inner)) return 'clock_timestamp()';
31
+
32
+ inner = inner.replace(TEXT_CAST_SUFFIX, '').trim();
33
+ if (NOW_LITERAL_PATTERN.test(inner)) return 'now()';
34
+
35
+ return undefined;
36
+ }
37
+
38
+ export function parseRawDefault(
39
+ rawDefault: string,
40
+ nativeType?: string,
41
+ ): ColumnDefault | undefined {
42
+ const trimmed = rawDefault.trim();
43
+ const normalizedType = nativeType?.toLowerCase();
44
+
45
+ if (NEXTVAL_PATTERN.test(trimmed)) {
46
+ return { kind: 'function', expression: 'autoincrement()' };
47
+ }
48
+
49
+ const canonicalTimestamp = canonicalizeTimestampDefault(trimmed);
50
+ if (canonicalTimestamp) {
51
+ return { kind: 'function', expression: canonicalTimestamp };
52
+ }
53
+
54
+ if (UUID_PATTERN.test(trimmed) || UUID_OSSP_PATTERN.test(trimmed)) {
55
+ return { kind: 'function', expression: 'gen_random_uuid()' };
56
+ }
57
+
58
+ if (NULL_PATTERN.test(trimmed)) {
59
+ return { kind: 'literal', value: null };
60
+ }
61
+
62
+ if (TRUE_PATTERN.test(trimmed)) {
63
+ return { kind: 'literal', value: true };
64
+ }
65
+
66
+ if (FALSE_PATTERN.test(trimmed)) {
67
+ return { kind: 'literal', value: false };
68
+ }
69
+
70
+ if (NUMERIC_PATTERN.test(trimmed)) {
71
+ return { kind: 'literal', value: Number(trimmed) };
72
+ }
73
+
74
+ const stringMatch = trimmed.match(STRING_LITERAL_PATTERN);
75
+ if (stringMatch?.[1] !== undefined) {
76
+ const unescaped = stringMatch[1].replace(/''/g, "'");
77
+ if (normalizedType === 'json' || normalizedType === 'jsonb') {
78
+ if (JSON_CAST_SUFFIX.test(trimmed)) {
79
+ return { kind: 'function', expression: trimmed };
80
+ }
81
+ try {
82
+ return { kind: 'literal', value: JSON.parse(unescaped) };
83
+ } catch {
84
+ // Fall through to the string form for malformed/non-JSON values.
85
+ }
86
+ }
87
+ return { kind: 'literal', value: unescaped };
88
+ }
89
+
90
+ return { kind: 'function', expression: trimmed };
91
+ }