@prisma-next/adapter-postgres 0.3.0-dev.4 → 0.3.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 (77) hide show
  1. package/README.md +64 -2
  2. package/dist/adapter-B5uYhMy7.mjs +266 -0
  3. package/dist/adapter-B5uYhMy7.mjs.map +1 -0
  4. package/dist/adapter.d.mts +23 -0
  5. package/dist/adapter.d.mts.map +1 -0
  6. package/dist/adapter.mjs +5 -0
  7. package/dist/codec-ids-Bsm9c7ns.mjs +29 -0
  8. package/dist/codec-ids-Bsm9c7ns.mjs.map +1 -0
  9. package/dist/codec-types.d.mts +141 -0
  10. package/dist/codec-types.d.mts.map +1 -0
  11. package/dist/codec-types.mjs +4 -0
  12. package/dist/codecs-BfC_5c-4.mjs +207 -0
  13. package/dist/codecs-BfC_5c-4.mjs.map +1 -0
  14. package/dist/column-types.d.mts +110 -0
  15. package/dist/column-types.d.mts.map +1 -0
  16. package/dist/column-types.mjs +180 -0
  17. package/dist/column-types.mjs.map +1 -0
  18. package/dist/control.d.mts +111 -0
  19. package/dist/control.d.mts.map +1 -0
  20. package/dist/control.mjs +427 -0
  21. package/dist/control.mjs.map +1 -0
  22. package/dist/descriptor-meta-ilnFI7bx.mjs +921 -0
  23. package/dist/descriptor-meta-ilnFI7bx.mjs.map +1 -0
  24. package/dist/runtime.d.mts +19 -0
  25. package/dist/runtime.d.mts.map +1 -0
  26. package/dist/runtime.mjs +85 -0
  27. package/dist/runtime.mjs.map +1 -0
  28. package/dist/sql-utils-CSfAGEwF.mjs +78 -0
  29. package/dist/sql-utils-CSfAGEwF.mjs.map +1 -0
  30. package/dist/types-CXO7EB60.d.mts +19 -0
  31. package/dist/types-CXO7EB60.d.mts.map +1 -0
  32. package/dist/types.d.mts +2 -0
  33. package/dist/types.mjs +1 -0
  34. package/package.json +39 -47
  35. package/src/core/adapter.ts +517 -0
  36. package/src/core/codec-ids.ts +28 -0
  37. package/src/core/codecs.ts +496 -0
  38. package/src/core/control-adapter.ts +536 -0
  39. package/src/core/default-normalizer.ts +90 -0
  40. package/src/core/descriptor-meta.ts +253 -0
  41. package/src/core/enum-control-hooks.ts +735 -0
  42. package/src/core/json-schema-type-expression.ts +131 -0
  43. package/src/core/json-schema-validator.ts +53 -0
  44. package/src/core/parameterized-types.ts +118 -0
  45. package/src/core/sql-utils.ts +111 -0
  46. package/src/core/standard-schema.ts +71 -0
  47. package/src/core/types.ts +53 -0
  48. package/src/exports/adapter.ts +1 -0
  49. package/src/exports/codec-types.ts +83 -0
  50. package/src/exports/column-types.ts +277 -0
  51. package/src/exports/control.ts +27 -0
  52. package/src/exports/runtime.ts +75 -0
  53. package/src/exports/types.ts +14 -0
  54. package/dist/exports/adapter.d.ts +0 -21
  55. package/dist/exports/adapter.js +0 -8
  56. package/dist/exports/adapter.js.map +0 -1
  57. package/dist/exports/chunk-B5SU5BVC.js +0 -47
  58. package/dist/exports/chunk-B5SU5BVC.js.map +0 -1
  59. package/dist/exports/chunk-CPAKRHXM.js +0 -162
  60. package/dist/exports/chunk-CPAKRHXM.js.map +0 -1
  61. package/dist/exports/chunk-ZHJOVBWT.js +0 -301
  62. package/dist/exports/chunk-ZHJOVBWT.js.map +0 -1
  63. package/dist/exports/codec-types.d.ts +0 -38
  64. package/dist/exports/codec-types.js +0 -7
  65. package/dist/exports/codec-types.js.map +0 -1
  66. package/dist/exports/column-types.d.ts +0 -20
  67. package/dist/exports/column-types.js +0 -49
  68. package/dist/exports/column-types.js.map +0 -1
  69. package/dist/exports/control.d.ts +0 -9
  70. package/dist/exports/control.js +0 -279
  71. package/dist/exports/control.js.map +0 -1
  72. package/dist/exports/runtime.d.ts +0 -17
  73. package/dist/exports/runtime.js +0 -20
  74. package/dist/exports/runtime.js.map +0 -1
  75. package/dist/exports/types.d.ts +0 -19
  76. package/dist/exports/types.js +0 -1
  77. package/dist/exports/types.js.map +0 -1
@@ -0,0 +1,536 @@
1
+ import type { ControlDriverInstance } from '@prisma-next/core-control-plane/types';
2
+ import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
3
+ import type {
4
+ PrimaryKey,
5
+ SqlColumnIR,
6
+ SqlForeignKeyIR,
7
+ SqlIndexIR,
8
+ SqlSchemaIR,
9
+ SqlTableIR,
10
+ SqlUniqueIR,
11
+ } from '@prisma-next/sql-schema-ir/types';
12
+ import { ifDefined } from '@prisma-next/utils/defined';
13
+ import { parsePostgresDefault } from './default-normalizer';
14
+ import { pgEnumControlHooks } from './enum-control-hooks';
15
+
16
+ /**
17
+ * Postgres control plane adapter for control-plane operations like introspection.
18
+ * Provides target-specific implementations for control-plane domain actions.
19
+ */
20
+ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
21
+ readonly familyId = 'sql' as const;
22
+ readonly targetId = 'postgres' as const;
23
+ /**
24
+ * @deprecated Use targetId instead
25
+ */
26
+ readonly target = 'postgres' as const;
27
+
28
+ /**
29
+ * Target-specific normalizer for raw Postgres default expressions.
30
+ * Used by schema verification to normalize raw defaults before comparison.
31
+ */
32
+ readonly normalizeDefault = parsePostgresDefault;
33
+
34
+ /**
35
+ * Target-specific normalizer for Postgres schema native type names.
36
+ * Used by schema verification to normalize introspected type names
37
+ * before comparison with contract native types.
38
+ */
39
+ readonly normalizeNativeType = normalizeSchemaNativeType;
40
+
41
+ /**
42
+ * Introspects a Postgres database schema and returns a raw SqlSchemaIR.
43
+ *
44
+ * This is a pure schema discovery operation that queries the Postgres catalog
45
+ * and returns the schema structure without type mapping or contract enrichment.
46
+ * Type mapping and enrichment are handled separately by enrichment helpers.
47
+ *
48
+ * Uses batched queries to minimize database round trips (7 queries instead of 5T+3).
49
+ *
50
+ * @param driver - ControlDriverInstance<'sql', 'postgres'> instance for executing queries
51
+ * @param contractIR - Optional contract IR for contract-guided introspection (filtering, optimization)
52
+ * @param schema - Schema name to introspect (defaults to 'public')
53
+ * @returns Promise resolving to SqlSchemaIR representing the live database schema
54
+ */
55
+ async introspect(
56
+ driver: ControlDriverInstance<'sql', 'postgres'>,
57
+ _contractIR?: unknown,
58
+ schema = 'public',
59
+ ): Promise<SqlSchemaIR> {
60
+ // Execute all queries in parallel for efficiency (7 queries instead of 5T+3)
61
+ const [
62
+ tablesResult,
63
+ columnsResult,
64
+ pkResult,
65
+ fkResult,
66
+ uniqueResult,
67
+ indexResult,
68
+ extensionsResult,
69
+ ] = await Promise.all([
70
+ // Query all tables
71
+ driver.query<{ table_name: string }>(
72
+ `SELECT table_name
73
+ FROM information_schema.tables
74
+ WHERE table_schema = $1
75
+ AND table_type = 'BASE TABLE'
76
+ ORDER BY table_name`,
77
+ [schema],
78
+ ),
79
+ // Query all columns for all tables in schema
80
+ driver.query<{
81
+ table_name: string;
82
+ column_name: string;
83
+ data_type: string;
84
+ udt_name: string;
85
+ is_nullable: string;
86
+ character_maximum_length: number | null;
87
+ numeric_precision: number | null;
88
+ numeric_scale: number | null;
89
+ column_default: string | null;
90
+ formatted_type: string | null;
91
+ }>(
92
+ `SELECT
93
+ c.table_name,
94
+ column_name,
95
+ data_type,
96
+ udt_name,
97
+ is_nullable,
98
+ character_maximum_length,
99
+ numeric_precision,
100
+ numeric_scale,
101
+ column_default,
102
+ format_type(a.atttypid, a.atttypmod) AS formatted_type
103
+ FROM information_schema.columns c
104
+ JOIN pg_catalog.pg_class cl
105
+ ON cl.relname = c.table_name
106
+ JOIN pg_catalog.pg_namespace ns
107
+ ON ns.nspname = c.table_schema
108
+ AND ns.oid = cl.relnamespace
109
+ JOIN pg_catalog.pg_attribute a
110
+ ON a.attrelid = cl.oid
111
+ AND a.attname = c.column_name
112
+ AND a.attnum > 0
113
+ AND NOT a.attisdropped
114
+ WHERE c.table_schema = $1
115
+ ORDER BY c.table_name, c.ordinal_position`,
116
+ [schema],
117
+ ),
118
+ // Query all primary keys for all tables in schema
119
+ driver.query<{
120
+ table_name: string;
121
+ constraint_name: string;
122
+ column_name: string;
123
+ ordinal_position: number;
124
+ }>(
125
+ `SELECT
126
+ tc.table_name,
127
+ tc.constraint_name,
128
+ kcu.column_name,
129
+ kcu.ordinal_position
130
+ FROM information_schema.table_constraints tc
131
+ JOIN information_schema.key_column_usage kcu
132
+ ON tc.constraint_name = kcu.constraint_name
133
+ AND tc.table_schema = kcu.table_schema
134
+ AND tc.table_name = kcu.table_name
135
+ WHERE tc.table_schema = $1
136
+ AND tc.constraint_type = 'PRIMARY KEY'
137
+ ORDER BY tc.table_name, kcu.ordinal_position`,
138
+ [schema],
139
+ ),
140
+ // Query all foreign keys for all tables in schema
141
+ driver.query<{
142
+ table_name: string;
143
+ constraint_name: string;
144
+ column_name: string;
145
+ ordinal_position: number;
146
+ referenced_table_schema: string;
147
+ referenced_table_name: string;
148
+ referenced_column_name: string;
149
+ }>(
150
+ `SELECT
151
+ tc.table_name,
152
+ tc.constraint_name,
153
+ kcu.column_name,
154
+ kcu.ordinal_position,
155
+ ccu.table_schema AS referenced_table_schema,
156
+ ccu.table_name AS referenced_table_name,
157
+ ccu.column_name AS referenced_column_name
158
+ FROM information_schema.table_constraints tc
159
+ JOIN information_schema.key_column_usage kcu
160
+ ON tc.constraint_name = kcu.constraint_name
161
+ AND tc.table_schema = kcu.table_schema
162
+ AND tc.table_name = kcu.table_name
163
+ JOIN information_schema.constraint_column_usage ccu
164
+ ON ccu.constraint_name = tc.constraint_name
165
+ AND ccu.table_schema = tc.table_schema
166
+ WHERE tc.table_schema = $1
167
+ AND tc.constraint_type = 'FOREIGN KEY'
168
+ ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
169
+ [schema],
170
+ ),
171
+ // Query all unique constraints for all tables in schema (excluding PKs)
172
+ driver.query<{
173
+ table_name: string;
174
+ constraint_name: string;
175
+ column_name: string;
176
+ ordinal_position: number;
177
+ }>(
178
+ `SELECT
179
+ tc.table_name,
180
+ tc.constraint_name,
181
+ kcu.column_name,
182
+ kcu.ordinal_position
183
+ FROM information_schema.table_constraints tc
184
+ JOIN information_schema.key_column_usage kcu
185
+ ON tc.constraint_name = kcu.constraint_name
186
+ AND tc.table_schema = kcu.table_schema
187
+ AND tc.table_name = kcu.table_name
188
+ WHERE tc.table_schema = $1
189
+ AND tc.constraint_type = 'UNIQUE'
190
+ ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
191
+ [schema],
192
+ ),
193
+ // Query all indexes for all tables in schema (excluding constraints)
194
+ driver.query<{
195
+ tablename: string;
196
+ indexname: string;
197
+ indisunique: boolean;
198
+ attname: string;
199
+ attnum: number;
200
+ }>(
201
+ `SELECT
202
+ i.tablename,
203
+ i.indexname,
204
+ ix.indisunique,
205
+ a.attname,
206
+ a.attnum
207
+ FROM pg_indexes i
208
+ JOIN pg_class ic ON ic.relname = i.indexname
209
+ JOIN pg_namespace ins ON ins.oid = ic.relnamespace AND ins.nspname = $1
210
+ JOIN pg_index ix ON ix.indexrelid = ic.oid
211
+ JOIN pg_class t ON t.oid = ix.indrelid
212
+ JOIN pg_namespace tn ON tn.oid = t.relnamespace AND tn.nspname = $1
213
+ LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND a.attnum > 0
214
+ WHERE i.schemaname = $1
215
+ AND NOT EXISTS (
216
+ SELECT 1
217
+ FROM information_schema.table_constraints tc
218
+ WHERE tc.table_schema = $1
219
+ AND tc.table_name = i.tablename
220
+ AND tc.constraint_name = i.indexname
221
+ )
222
+ ORDER BY i.tablename, i.indexname, a.attnum`,
223
+ [schema],
224
+ ),
225
+ // Query extensions
226
+ driver.query<{ extname: string }>(
227
+ `SELECT extname
228
+ FROM pg_extension
229
+ ORDER BY extname`,
230
+ [],
231
+ ),
232
+ ]);
233
+
234
+ // Group results by table name for efficient lookup
235
+ const columnsByTable = groupBy(columnsResult.rows, 'table_name');
236
+ const pksByTable = groupBy(pkResult.rows, 'table_name');
237
+ const fksByTable = groupBy(fkResult.rows, 'table_name');
238
+ const uniquesByTable = groupBy(uniqueResult.rows, 'table_name');
239
+ const indexesByTable = groupBy(indexResult.rows, 'tablename');
240
+
241
+ // Get set of PK constraint names per table (to exclude from uniques)
242
+ const pkConstraintsByTable = new Map<string, Set<string>>();
243
+ for (const row of pkResult.rows) {
244
+ let constraints = pkConstraintsByTable.get(row.table_name);
245
+ if (!constraints) {
246
+ constraints = new Set();
247
+ pkConstraintsByTable.set(row.table_name, constraints);
248
+ }
249
+ constraints.add(row.constraint_name);
250
+ }
251
+
252
+ const tables: Record<string, SqlTableIR> = {};
253
+
254
+ for (const tableRow of tablesResult.rows) {
255
+ const tableName = tableRow.table_name;
256
+
257
+ // Process columns for this table
258
+ const columns: Record<string, SqlColumnIR> = {};
259
+ for (const colRow of columnsByTable.get(tableName) ?? []) {
260
+ let nativeType = colRow.udt_name;
261
+ const formattedType = colRow.formatted_type
262
+ ? normalizeFormattedType(colRow.formatted_type, colRow.data_type, colRow.udt_name)
263
+ : null;
264
+ if (formattedType) {
265
+ nativeType = formattedType;
266
+ } else if (colRow.data_type === 'character varying' || colRow.data_type === 'character') {
267
+ if (colRow.character_maximum_length) {
268
+ nativeType = `${colRow.data_type}(${colRow.character_maximum_length})`;
269
+ } else {
270
+ nativeType = colRow.data_type;
271
+ }
272
+ } else if (colRow.data_type === 'numeric' || colRow.data_type === 'decimal') {
273
+ if (colRow.numeric_precision && colRow.numeric_scale !== null) {
274
+ nativeType = `${colRow.data_type}(${colRow.numeric_precision},${colRow.numeric_scale})`;
275
+ } else if (colRow.numeric_precision) {
276
+ nativeType = `${colRow.data_type}(${colRow.numeric_precision})`;
277
+ } else {
278
+ nativeType = colRow.data_type;
279
+ }
280
+ } else {
281
+ nativeType = colRow.udt_name || colRow.data_type;
282
+ }
283
+
284
+ columns[colRow.column_name] = {
285
+ name: colRow.column_name,
286
+ nativeType,
287
+ nullable: colRow.is_nullable === 'YES',
288
+ ...ifDefined('default', colRow.column_default ?? undefined),
289
+ };
290
+ }
291
+
292
+ // Process primary key
293
+ const pkRows = [...(pksByTable.get(tableName) ?? [])];
294
+ const primaryKeyColumns = pkRows
295
+ .sort((a, b) => a.ordinal_position - b.ordinal_position)
296
+ .map((row) => row.column_name);
297
+ const primaryKey: PrimaryKey | undefined =
298
+ primaryKeyColumns.length > 0
299
+ ? {
300
+ columns: primaryKeyColumns,
301
+ ...(pkRows[0]?.constraint_name ? { name: pkRows[0].constraint_name } : {}),
302
+ }
303
+ : undefined;
304
+
305
+ // Process foreign keys
306
+ const foreignKeysMap = new Map<
307
+ string,
308
+ { columns: string[]; referencedTable: string; referencedColumns: string[]; name: string }
309
+ >();
310
+ for (const fkRow of fksByTable.get(tableName) ?? []) {
311
+ const existing = foreignKeysMap.get(fkRow.constraint_name);
312
+ if (existing) {
313
+ existing.columns.push(fkRow.column_name);
314
+ existing.referencedColumns.push(fkRow.referenced_column_name);
315
+ } else {
316
+ foreignKeysMap.set(fkRow.constraint_name, {
317
+ columns: [fkRow.column_name],
318
+ referencedTable: fkRow.referenced_table_name,
319
+ referencedColumns: [fkRow.referenced_column_name],
320
+ name: fkRow.constraint_name,
321
+ });
322
+ }
323
+ }
324
+ const foreignKeys: readonly SqlForeignKeyIR[] = Array.from(foreignKeysMap.values()).map(
325
+ (fk) => ({
326
+ columns: Object.freeze([...fk.columns]) as readonly string[],
327
+ referencedTable: fk.referencedTable,
328
+ referencedColumns: Object.freeze([...fk.referencedColumns]) as readonly string[],
329
+ name: fk.name,
330
+ }),
331
+ );
332
+
333
+ // Process unique constraints (excluding those that are also PKs)
334
+ const pkConstraints = pkConstraintsByTable.get(tableName) ?? new Set();
335
+ const uniquesMap = new Map<string, { columns: string[]; name: string }>();
336
+ for (const uniqueRow of uniquesByTable.get(tableName) ?? []) {
337
+ // Skip if this constraint is also a primary key
338
+ if (pkConstraints.has(uniqueRow.constraint_name)) {
339
+ continue;
340
+ }
341
+ const existing = uniquesMap.get(uniqueRow.constraint_name);
342
+ if (existing) {
343
+ existing.columns.push(uniqueRow.column_name);
344
+ } else {
345
+ uniquesMap.set(uniqueRow.constraint_name, {
346
+ columns: [uniqueRow.column_name],
347
+ name: uniqueRow.constraint_name,
348
+ });
349
+ }
350
+ }
351
+ const uniques: readonly SqlUniqueIR[] = Array.from(uniquesMap.values()).map((uq) => ({
352
+ columns: Object.freeze([...uq.columns]) as readonly string[],
353
+ name: uq.name,
354
+ }));
355
+
356
+ // Process indexes
357
+ const indexesMap = new Map<string, { columns: string[]; name: string; unique: boolean }>();
358
+ for (const idxRow of indexesByTable.get(tableName) ?? []) {
359
+ if (!idxRow.attname) {
360
+ continue;
361
+ }
362
+ const existing = indexesMap.get(idxRow.indexname);
363
+ if (existing) {
364
+ existing.columns.push(idxRow.attname);
365
+ } else {
366
+ indexesMap.set(idxRow.indexname, {
367
+ columns: [idxRow.attname],
368
+ name: idxRow.indexname,
369
+ unique: idxRow.indisunique,
370
+ });
371
+ }
372
+ }
373
+ const indexes: readonly SqlIndexIR[] = Array.from(indexesMap.values()).map((idx) => ({
374
+ columns: Object.freeze([...idx.columns]) as readonly string[],
375
+ name: idx.name,
376
+ unique: idx.unique,
377
+ }));
378
+
379
+ tables[tableName] = {
380
+ name: tableName,
381
+ columns,
382
+ ...ifDefined('primaryKey', primaryKey),
383
+ foreignKeys,
384
+ uniques,
385
+ indexes,
386
+ };
387
+ }
388
+
389
+ const extensions = extensionsResult.rows.map((row) => row.extname);
390
+
391
+ const storageTypes =
392
+ (await pgEnumControlHooks.introspectTypes?.({ driver, schemaName: schema })) ?? {};
393
+
394
+ const annotations = {
395
+ pg: {
396
+ schema,
397
+ version: await this.getPostgresVersion(driver),
398
+ ...ifDefined(
399
+ 'storageTypes',
400
+ Object.keys(storageTypes).length > 0 ? storageTypes : undefined,
401
+ ),
402
+ },
403
+ };
404
+
405
+ return {
406
+ tables,
407
+ extensions,
408
+ annotations,
409
+ };
410
+ }
411
+
412
+ /**
413
+ * Gets the Postgres version from the database.
414
+ */
415
+ private async getPostgresVersion(
416
+ driver: ControlDriverInstance<'sql', 'postgres'>,
417
+ ): Promise<string> {
418
+ const result = await driver.query<{ version: string }>('SELECT version() AS version', []);
419
+ const versionString = result.rows[0]?.version ?? '';
420
+ // Extract version number from "PostgreSQL 15.1 ..." format
421
+ const match = versionString.match(/PostgreSQL (\d+\.\d+)/);
422
+ return match?.[1] ?? 'unknown';
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Pre-computed lookup map for simple prefix-based type normalization.
428
+ * Maps short Postgres type names to their canonical SQL names.
429
+ * Using a Map for O(1) lookup instead of multiple startsWith checks.
430
+ */
431
+ const TYPE_PREFIX_MAP: ReadonlyMap<string, string> = new Map([
432
+ ['varchar', 'character varying'],
433
+ ['bpchar', 'character'],
434
+ ['varbit', 'bit varying'],
435
+ ]);
436
+
437
+ /**
438
+ * Normalizes a Postgres schema native type to its canonical form for comparison.
439
+ *
440
+ * Uses a pre-computed lookup map for simple prefix replacements (O(1))
441
+ * and handles complex temporal type normalization separately.
442
+ */
443
+ export function normalizeSchemaNativeType(nativeType: string): string {
444
+ const trimmed = nativeType.trim();
445
+
446
+ // Fast path: check simple prefix replacements using the lookup map
447
+ for (const [prefix, replacement] of TYPE_PREFIX_MAP) {
448
+ if (trimmed.startsWith(prefix)) {
449
+ return replacement + trimmed.slice(prefix.length);
450
+ }
451
+ }
452
+
453
+ // Temporal types with time zone handling
454
+ // Check for 'with time zone' suffix first (more specific)
455
+ if (trimmed.includes(' with time zone')) {
456
+ if (trimmed.startsWith('timestamp')) {
457
+ return `timestamptz${trimmed.slice(9).replace(' with time zone', '')}`;
458
+ }
459
+ if (trimmed.startsWith('time')) {
460
+ return `timetz${trimmed.slice(4).replace(' with time zone', '')}`;
461
+ }
462
+ }
463
+
464
+ // Handle 'without time zone' suffix - just strip it
465
+ if (trimmed.includes(' without time zone')) {
466
+ return trimmed.replace(' without time zone', '');
467
+ }
468
+
469
+ return trimmed;
470
+ }
471
+
472
+ function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
473
+ if (formattedType === 'integer') {
474
+ return 'int4';
475
+ }
476
+ if (formattedType === 'smallint') {
477
+ return 'int2';
478
+ }
479
+ if (formattedType === 'bigint') {
480
+ return 'int8';
481
+ }
482
+ if (formattedType === 'real') {
483
+ return 'float4';
484
+ }
485
+ if (formattedType === 'double precision') {
486
+ return 'float8';
487
+ }
488
+ if (formattedType === 'boolean') {
489
+ return 'bool';
490
+ }
491
+ if (formattedType.startsWith('varchar')) {
492
+ return formattedType.replace('varchar', 'character varying');
493
+ }
494
+ if (formattedType.startsWith('bpchar')) {
495
+ return formattedType.replace('bpchar', 'character');
496
+ }
497
+ if (formattedType.startsWith('varbit')) {
498
+ return formattedType.replace('varbit', 'bit varying');
499
+ }
500
+ if (dataType === 'timestamp with time zone' || udtName === 'timestamptz') {
501
+ return formattedType.replace('timestamp', 'timestamptz').replace(' with time zone', '').trim();
502
+ }
503
+ if (dataType === 'timestamp without time zone' || udtName === 'timestamp') {
504
+ return formattedType.replace(' without time zone', '').trim();
505
+ }
506
+ if (dataType === 'time with time zone' || udtName === 'timetz') {
507
+ return formattedType.replace('time', 'timetz').replace(' with time zone', '').trim();
508
+ }
509
+ if (dataType === 'time without time zone' || udtName === 'time') {
510
+ return formattedType.replace(' without time zone', '').trim();
511
+ }
512
+ // Only dataType === 'USER-DEFINED' should ever be quoted, but this should be safe without
513
+ // checking that explicitly either way
514
+ if (formattedType.startsWith('"') && formattedType.endsWith('"')) {
515
+ return formattedType.slice(1, -1);
516
+ }
517
+ return formattedType;
518
+ }
519
+
520
+ /**
521
+ * Groups an array of objects by a specified key.
522
+ * Returns a Map for O(1) lookup by group key.
523
+ */
524
+ function groupBy<T, K extends keyof T>(items: readonly T[], key: K): Map<T[K], T[]> {
525
+ const map = new Map<T[K], T[]>();
526
+ for (const item of items) {
527
+ const groupKey = item[key];
528
+ let group = map.get(groupKey);
529
+ if (!group) {
530
+ group = [];
531
+ map.set(groupKey, group);
532
+ }
533
+ group.push(item);
534
+ }
535
+ return map;
536
+ }
@@ -0,0 +1,90 @@
1
+ import type { ColumnDefault } from '@prisma-next/contract/types';
2
+
3
+ /**
4
+ * Pre-compiled regex patterns for performance.
5
+ * These are compiled once at module load time rather than on each function call.
6
+ */
7
+ const NEXTVAL_PATTERN = /^nextval\s*\(/i;
8
+ const TIMESTAMP_PATTERN = /^(now\s*\(\s*\)|CURRENT_TIMESTAMP|clock_timestamp\s*\(\s*\))$/i;
9
+ const UUID_PATTERN = /^gen_random_uuid\s*\(\s*\)$/i;
10
+ const UUID_OSSP_PATTERN = /^uuid_generate_v4\s*\(\s*\)$/i;
11
+ const TRUE_PATTERN = /^true$/i;
12
+ const FALSE_PATTERN = /^false$/i;
13
+ const NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
14
+ const STRING_LITERAL_PATTERN = /^'((?:[^']|'')*)'(?:::(?:"[^"]+"|[\w\s]+)(?:\(\d+\))?)?$/;
15
+
16
+ /**
17
+ * Parses a raw Postgres column default expression into a normalized ColumnDefault.
18
+ * This enables semantic comparison between contract defaults and introspected schema defaults.
19
+ *
20
+ * Used by the migration diff layer to normalize raw database defaults during comparison,
21
+ * keeping the introspection layer focused on faithful data capture.
22
+ *
23
+ * @param rawDefault - Raw default expression from information_schema.columns.column_default
24
+ * @param _nativeType - Native column type (currently unused, reserved for future type-aware parsing)
25
+ * @returns Normalized ColumnDefault or undefined if the expression cannot be parsed
26
+ */
27
+ export function parsePostgresDefault(
28
+ rawDefault: string,
29
+ _nativeType?: string,
30
+ ): ColumnDefault | undefined {
31
+ const trimmed = rawDefault.trim();
32
+ const normalizedType = _nativeType?.toLowerCase();
33
+ const isBigInt = normalizedType === 'bigint' || normalizedType === 'int8';
34
+
35
+ // Autoincrement: nextval('tablename_column_seq'::regclass)
36
+ if (NEXTVAL_PATTERN.test(trimmed)) {
37
+ return { kind: 'function', expression: 'autoincrement()' };
38
+ }
39
+
40
+ // now() / CURRENT_TIMESTAMP / clock_timestamp()
41
+ if (TIMESTAMP_PATTERN.test(trimmed)) {
42
+ return { kind: 'function', expression: 'now()' };
43
+ }
44
+
45
+ // gen_random_uuid()
46
+ if (UUID_PATTERN.test(trimmed)) {
47
+ return { kind: 'function', expression: 'gen_random_uuid()' };
48
+ }
49
+
50
+ // uuid_generate_v4() from uuid-ossp extension
51
+ if (UUID_OSSP_PATTERN.test(trimmed)) {
52
+ return { kind: 'function', expression: 'gen_random_uuid()' };
53
+ }
54
+
55
+ // Boolean literals
56
+ if (TRUE_PATTERN.test(trimmed)) {
57
+ return { kind: 'literal', value: true };
58
+ }
59
+ if (FALSE_PATTERN.test(trimmed)) {
60
+ return { kind: 'literal', value: false };
61
+ }
62
+
63
+ // Numeric literals (integer or decimal)
64
+ if (NUMERIC_PATTERN.test(trimmed)) {
65
+ if (isBigInt) {
66
+ return { kind: 'literal', value: { $type: 'bigint', value: trimmed } };
67
+ }
68
+ return { kind: 'literal', value: Number(trimmed) };
69
+ }
70
+
71
+ // String literals: 'value'::type or just 'value'
72
+ // Match: 'some text'::text, 'hello'::character varying, 'value', etc.
73
+ // Strip the ::type cast so the normalized expression matches what contract authors write.
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
+ try {
79
+ return { kind: 'literal', value: JSON.parse(unescaped) };
80
+ } catch {
81
+ // Keep legacy behavior for malformed/non-JSON string content.
82
+ }
83
+ }
84
+ return { kind: 'literal', value: unescaped };
85
+ }
86
+
87
+ // Unrecognized expression - return as a function with the raw expression
88
+ // This preserves the information for debugging while still being comparable
89
+ return { kind: 'function', expression: trimmed };
90
+ }