@prisma-next/adapter-postgres 0.3.0-dev.6 → 0.3.0-dev.63

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 (91) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +64 -2
  3. package/dist/adapter-DtehReRR.mjs +271 -0
  4. package/dist/adapter-DtehReRR.mjs.map +1 -0
  5. package/dist/adapter.d.mts +23 -0
  6. package/dist/adapter.d.mts.map +1 -0
  7. package/dist/adapter.mjs +5 -0
  8. package/dist/codec-ids-Bsm9c7ns.mjs +29 -0
  9. package/dist/codec-ids-Bsm9c7ns.mjs.map +1 -0
  10. package/dist/codec-types.d.mts +141 -0
  11. package/dist/codec-types.d.mts.map +1 -0
  12. package/dist/codec-types.mjs +4 -0
  13. package/dist/codecs-BfC_5c-4.mjs +207 -0
  14. package/dist/codecs-BfC_5c-4.mjs.map +1 -0
  15. package/dist/column-types.d.mts +110 -0
  16. package/dist/column-types.d.mts.map +1 -0
  17. package/dist/column-types.mjs +180 -0
  18. package/dist/column-types.mjs.map +1 -0
  19. package/dist/control.d.mts +111 -0
  20. package/dist/control.d.mts.map +1 -0
  21. package/dist/control.mjs +463 -0
  22. package/dist/control.mjs.map +1 -0
  23. package/dist/descriptor-meta-ilnFI7bx.mjs +921 -0
  24. package/dist/descriptor-meta-ilnFI7bx.mjs.map +1 -0
  25. package/dist/runtime.d.mts +19 -0
  26. package/dist/runtime.d.mts.map +1 -0
  27. package/dist/runtime.mjs +85 -0
  28. package/dist/runtime.mjs.map +1 -0
  29. package/dist/sql-utils-CSfAGEwF.mjs +78 -0
  30. package/dist/sql-utils-CSfAGEwF.mjs.map +1 -0
  31. package/dist/types-CXO7EB60.d.mts +19 -0
  32. package/dist/types-CXO7EB60.d.mts.map +1 -0
  33. package/dist/types.d.mts +2 -0
  34. package/dist/types.mjs +1 -0
  35. package/package.json +37 -46
  36. package/src/core/adapter.ts +139 -28
  37. package/src/core/codec-ids.ts +28 -0
  38. package/src/core/codecs.ts +325 -23
  39. package/src/core/control-adapter.ts +400 -178
  40. package/src/core/default-normalizer.ts +90 -0
  41. package/src/core/descriptor-meta.ts +221 -9
  42. package/src/core/enum-control-hooks.ts +735 -0
  43. package/src/core/json-schema-type-expression.ts +131 -0
  44. package/src/core/json-schema-validator.ts +53 -0
  45. package/src/core/parameterized-types.ts +118 -0
  46. package/src/core/sql-utils.ts +111 -0
  47. package/src/core/standard-schema.ts +71 -0
  48. package/src/exports/codec-types.ts +73 -1
  49. package/src/exports/column-types.ts +233 -9
  50. package/src/exports/control.ts +16 -9
  51. package/src/exports/runtime.ts +61 -18
  52. package/dist/chunk-HD5YISNQ.js +0 -47
  53. package/dist/chunk-HD5YISNQ.js.map +0 -1
  54. package/dist/chunk-J3XSOAM2.js +0 -162
  55. package/dist/chunk-J3XSOAM2.js.map +0 -1
  56. package/dist/chunk-T6S3A6VT.js +0 -301
  57. package/dist/chunk-T6S3A6VT.js.map +0 -1
  58. package/dist/core/adapter.d.ts +0 -19
  59. package/dist/core/adapter.d.ts.map +0 -1
  60. package/dist/core/codecs.d.ts +0 -110
  61. package/dist/core/codecs.d.ts.map +0 -1
  62. package/dist/core/control-adapter.d.ts +0 -33
  63. package/dist/core/control-adapter.d.ts.map +0 -1
  64. package/dist/core/descriptor-meta.d.ts +0 -72
  65. package/dist/core/descriptor-meta.d.ts.map +0 -1
  66. package/dist/core/types.d.ts +0 -16
  67. package/dist/core/types.d.ts.map +0 -1
  68. package/dist/exports/adapter.d.ts +0 -2
  69. package/dist/exports/adapter.d.ts.map +0 -1
  70. package/dist/exports/adapter.js +0 -8
  71. package/dist/exports/adapter.js.map +0 -1
  72. package/dist/exports/codec-types.d.ts +0 -11
  73. package/dist/exports/codec-types.d.ts.map +0 -1
  74. package/dist/exports/codec-types.js +0 -7
  75. package/dist/exports/codec-types.js.map +0 -1
  76. package/dist/exports/column-types.d.ts +0 -17
  77. package/dist/exports/column-types.d.ts.map +0 -1
  78. package/dist/exports/column-types.js +0 -49
  79. package/dist/exports/column-types.js.map +0 -1
  80. package/dist/exports/control.d.ts +0 -8
  81. package/dist/exports/control.d.ts.map +0 -1
  82. package/dist/exports/control.js +0 -279
  83. package/dist/exports/control.js.map +0 -1
  84. package/dist/exports/runtime.d.ts +0 -15
  85. package/dist/exports/runtime.d.ts.map +0 -1
  86. package/dist/exports/runtime.js +0 -20
  87. package/dist/exports/runtime.js.map +0 -1
  88. package/dist/exports/types.d.ts +0 -2
  89. package/dist/exports/types.d.ts.map +0 -1
  90. package/dist/exports/types.js +0 -1
  91. package/dist/exports/types.js.map +0 -1
@@ -5,10 +5,14 @@ import type {
5
5
  SqlColumnIR,
6
6
  SqlForeignKeyIR,
7
7
  SqlIndexIR,
8
+ SqlReferentialAction,
8
9
  SqlSchemaIR,
9
10
  SqlTableIR,
10
11
  SqlUniqueIR,
11
12
  } from '@prisma-next/sql-schema-ir/types';
13
+ import { ifDefined } from '@prisma-next/utils/defined';
14
+ import { parsePostgresDefault } from './default-normalizer';
15
+ import { pgEnumControlHooks } from './enum-control-hooks';
12
16
 
13
17
  /**
14
18
  * Postgres control plane adapter for control-plane operations like introspection.
@@ -22,6 +26,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
22
26
  */
23
27
  readonly target = 'postgres' as const;
24
28
 
29
+ /**
30
+ * Target-specific normalizer for raw Postgres default expressions.
31
+ * Used by schema verification to normalize raw defaults before comparison.
32
+ */
33
+ readonly normalizeDefault = parsePostgresDefault;
34
+
35
+ /**
36
+ * Target-specific normalizer for Postgres schema native type names.
37
+ * Used by schema verification to normalize introspected type names
38
+ * before comparison with contract native types.
39
+ */
40
+ readonly normalizeNativeType = normalizeSchemaNativeType;
41
+
25
42
  /**
26
43
  * Introspects a Postgres database schema and returns a raw SqlSchemaIR.
27
44
  *
@@ -29,6 +46,8 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
29
46
  * and returns the schema structure without type mapping or contract enrichment.
30
47
  * Type mapping and enrichment are handled separately by enrichment helpers.
31
48
  *
49
+ * Uses batched queries to minimize database round trips (7 queries instead of 5T+3).
50
+ *
32
51
  * @param driver - ControlDriverInstance<'sql', 'postgres'> instance for executing queries
33
52
  * @param contractIR - Optional contract IR for contract-guided introspection (filtering, optimization)
34
53
  * @param schema - Schema name to introspect (defaults to 'public')
@@ -39,25 +58,28 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
39
58
  _contractIR?: unknown,
40
59
  schema = 'public',
41
60
  ): Promise<SqlSchemaIR> {
42
- // Query tables
43
- const tablesResult = await driver.query<{
44
- table_name: string;
45
- }>(
46
- `SELECT table_name
47
- FROM information_schema.tables
48
- WHERE table_schema = $1
49
- AND table_type = 'BASE TABLE'
50
- ORDER BY table_name`,
51
- [schema],
52
- );
53
-
54
- const tables: Record<string, SqlTableIR> = {};
55
-
56
- for (const tableRow of tablesResult.rows) {
57
- const tableName = tableRow.table_name;
58
-
59
- // Query columns for this table
60
- const columnsResult = await driver.query<{
61
+ // Execute all queries in parallel for efficiency (7 queries instead of 5T+3)
62
+ const [
63
+ tablesResult,
64
+ columnsResult,
65
+ pkResult,
66
+ fkResult,
67
+ uniqueResult,
68
+ indexResult,
69
+ extensionsResult,
70
+ ] = await Promise.all([
71
+ // Query all tables
72
+ driver.query<{ table_name: string }>(
73
+ `SELECT table_name
74
+ FROM information_schema.tables
75
+ WHERE table_schema = $1
76
+ AND table_type = 'BASE TABLE'
77
+ ORDER BY table_name`,
78
+ [schema],
79
+ ),
80
+ // Query all columns for all tables in schema
81
+ driver.query<{
82
+ table_name: string;
61
83
  column_name: string;
62
84
  data_type: string;
63
85
  udt_name: string;
@@ -65,27 +87,203 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
65
87
  character_maximum_length: number | null;
66
88
  numeric_precision: number | null;
67
89
  numeric_scale: number | null;
90
+ column_default: string | null;
91
+ formatted_type: string | null;
68
92
  }>(
69
93
  `SELECT
94
+ c.table_name,
70
95
  column_name,
71
96
  data_type,
72
97
  udt_name,
73
98
  is_nullable,
74
99
  character_maximum_length,
75
100
  numeric_precision,
76
- numeric_scale
77
- FROM information_schema.columns
78
- WHERE table_schema = $1
79
- AND table_name = $2
80
- ORDER BY ordinal_position`,
81
- [schema, tableName],
82
- );
101
+ numeric_scale,
102
+ column_default,
103
+ format_type(a.atttypid, a.atttypmod) AS formatted_type
104
+ FROM information_schema.columns c
105
+ JOIN pg_catalog.pg_class cl
106
+ ON cl.relname = c.table_name
107
+ JOIN pg_catalog.pg_namespace ns
108
+ ON ns.nspname = c.table_schema
109
+ AND ns.oid = cl.relnamespace
110
+ JOIN pg_catalog.pg_attribute a
111
+ ON a.attrelid = cl.oid
112
+ AND a.attname = c.column_name
113
+ AND a.attnum > 0
114
+ AND NOT a.attisdropped
115
+ WHERE c.table_schema = $1
116
+ ORDER BY c.table_name, c.ordinal_position`,
117
+ [schema],
118
+ ),
119
+ // Query all primary keys for all tables in schema
120
+ driver.query<{
121
+ table_name: string;
122
+ constraint_name: string;
123
+ column_name: string;
124
+ ordinal_position: number;
125
+ }>(
126
+ `SELECT
127
+ tc.table_name,
128
+ tc.constraint_name,
129
+ kcu.column_name,
130
+ kcu.ordinal_position
131
+ FROM information_schema.table_constraints tc
132
+ JOIN information_schema.key_column_usage kcu
133
+ ON tc.constraint_name = kcu.constraint_name
134
+ AND tc.table_schema = kcu.table_schema
135
+ AND tc.table_name = kcu.table_name
136
+ WHERE tc.table_schema = $1
137
+ AND tc.constraint_type = 'PRIMARY KEY'
138
+ ORDER BY tc.table_name, kcu.ordinal_position`,
139
+ [schema],
140
+ ),
141
+ // Query all foreign keys for all tables in schema, including referential actions.
142
+ // Uses pg_catalog for correct positional pairing of composite FK columns
143
+ // (information_schema.constraint_column_usage lacks ordinal_position,
144
+ // which causes Cartesian products for multi-column FKs).
145
+ driver.query<{
146
+ table_name: string;
147
+ constraint_name: string;
148
+ column_name: string;
149
+ ordinal_position: number;
150
+ referenced_table_schema: string;
151
+ referenced_table_name: string;
152
+ referenced_column_name: string;
153
+ delete_rule: string;
154
+ update_rule: string;
155
+ }>(
156
+ `SELECT
157
+ tc.table_name,
158
+ tc.constraint_name,
159
+ kcu.column_name,
160
+ kcu.ordinal_position,
161
+ ref_ns.nspname AS referenced_table_schema,
162
+ ref_cl.relname AS referenced_table_name,
163
+ ref_att.attname AS referenced_column_name,
164
+ rc.delete_rule,
165
+ rc.update_rule
166
+ FROM information_schema.table_constraints tc
167
+ JOIN information_schema.key_column_usage kcu
168
+ ON tc.constraint_name = kcu.constraint_name
169
+ AND tc.table_schema = kcu.table_schema
170
+ AND tc.table_name = kcu.table_name
171
+ JOIN pg_catalog.pg_constraint pgc
172
+ ON pgc.conname = tc.constraint_name
173
+ AND pgc.connamespace = (
174
+ SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = tc.table_schema
175
+ )
176
+ JOIN pg_catalog.pg_class ref_cl
177
+ ON ref_cl.oid = pgc.confrelid
178
+ JOIN pg_catalog.pg_namespace ref_ns
179
+ ON ref_ns.oid = ref_cl.relnamespace
180
+ JOIN pg_catalog.pg_attribute ref_att
181
+ ON ref_att.attrelid = pgc.confrelid
182
+ AND ref_att.attnum = pgc.confkey[kcu.ordinal_position]
183
+ JOIN information_schema.referential_constraints rc
184
+ ON rc.constraint_name = tc.constraint_name
185
+ AND rc.constraint_schema = tc.table_schema
186
+ WHERE tc.table_schema = $1
187
+ AND tc.constraint_type = 'FOREIGN KEY'
188
+ ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
189
+ [schema],
190
+ ),
191
+ // Query all unique constraints for all tables in schema (excluding PKs)
192
+ driver.query<{
193
+ table_name: string;
194
+ constraint_name: string;
195
+ column_name: string;
196
+ ordinal_position: number;
197
+ }>(
198
+ `SELECT
199
+ tc.table_name,
200
+ tc.constraint_name,
201
+ kcu.column_name,
202
+ kcu.ordinal_position
203
+ FROM information_schema.table_constraints tc
204
+ JOIN information_schema.key_column_usage kcu
205
+ ON tc.constraint_name = kcu.constraint_name
206
+ AND tc.table_schema = kcu.table_schema
207
+ AND tc.table_name = kcu.table_name
208
+ WHERE tc.table_schema = $1
209
+ AND tc.constraint_type = 'UNIQUE'
210
+ ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
211
+ [schema],
212
+ ),
213
+ // Query all indexes for all tables in schema (excluding constraints)
214
+ driver.query<{
215
+ tablename: string;
216
+ indexname: string;
217
+ indisunique: boolean;
218
+ attname: string;
219
+ attnum: number;
220
+ }>(
221
+ `SELECT
222
+ i.tablename,
223
+ i.indexname,
224
+ ix.indisunique,
225
+ a.attname,
226
+ a.attnum
227
+ FROM pg_indexes i
228
+ JOIN pg_class ic ON ic.relname = i.indexname
229
+ JOIN pg_namespace ins ON ins.oid = ic.relnamespace AND ins.nspname = $1
230
+ JOIN pg_index ix ON ix.indexrelid = ic.oid
231
+ JOIN pg_class t ON t.oid = ix.indrelid
232
+ JOIN pg_namespace tn ON tn.oid = t.relnamespace AND tn.nspname = $1
233
+ LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND a.attnum > 0
234
+ WHERE i.schemaname = $1
235
+ AND NOT EXISTS (
236
+ SELECT 1
237
+ FROM information_schema.table_constraints tc
238
+ WHERE tc.table_schema = $1
239
+ AND tc.table_name = i.tablename
240
+ AND tc.constraint_name = i.indexname
241
+ )
242
+ ORDER BY i.tablename, i.indexname, a.attnum`,
243
+ [schema],
244
+ ),
245
+ // Query extensions
246
+ driver.query<{ extname: string }>(
247
+ `SELECT extname
248
+ FROM pg_extension
249
+ ORDER BY extname`,
250
+ [],
251
+ ),
252
+ ]);
253
+
254
+ // Group results by table name for efficient lookup
255
+ const columnsByTable = groupBy(columnsResult.rows, 'table_name');
256
+ const pksByTable = groupBy(pkResult.rows, 'table_name');
257
+ const fksByTable = groupBy(fkResult.rows, 'table_name');
258
+ const uniquesByTable = groupBy(uniqueResult.rows, 'table_name');
259
+ const indexesByTable = groupBy(indexResult.rows, 'tablename');
83
260
 
261
+ // Get set of PK constraint names per table (to exclude from uniques)
262
+ const pkConstraintsByTable = new Map<string, Set<string>>();
263
+ for (const row of pkResult.rows) {
264
+ let constraints = pkConstraintsByTable.get(row.table_name);
265
+ if (!constraints) {
266
+ constraints = new Set();
267
+ pkConstraintsByTable.set(row.table_name, constraints);
268
+ }
269
+ constraints.add(row.constraint_name);
270
+ }
271
+
272
+ const tables: Record<string, SqlTableIR> = {};
273
+
274
+ for (const tableRow of tablesResult.rows) {
275
+ const tableName = tableRow.table_name;
276
+
277
+ // Process columns for this table
84
278
  const columns: Record<string, SqlColumnIR> = {};
85
- for (const colRow of columnsResult.rows) {
86
- // Build native type string from catalog data
279
+ for (const colRow of columnsByTable.get(tableName) ?? []) {
87
280
  let nativeType = colRow.udt_name;
88
- if (colRow.data_type === 'character varying' || colRow.data_type === 'character') {
281
+ const formattedType = colRow.formatted_type
282
+ ? normalizeFormattedType(colRow.formatted_type, colRow.data_type, colRow.udt_name)
283
+ : null;
284
+ if (formattedType) {
285
+ nativeType = formattedType;
286
+ } else if (colRow.data_type === 'character varying' || colRow.data_type === 'character') {
89
287
  if (colRow.character_maximum_length) {
90
288
  nativeType = `${colRow.data_type}(${colRow.character_maximum_length})`;
91
289
  } else {
@@ -107,75 +305,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
107
305
  name: colRow.column_name,
108
306
  nativeType,
109
307
  nullable: colRow.is_nullable === 'YES',
308
+ ...ifDefined('default', colRow.column_default ?? undefined),
110
309
  };
111
310
  }
112
311
 
113
- // Query primary key
114
- const pkResult = await driver.query<{
115
- constraint_name: string;
116
- column_name: string;
117
- ordinal_position: number;
118
- }>(
119
- `SELECT
120
- tc.constraint_name,
121
- kcu.column_name,
122
- kcu.ordinal_position
123
- FROM information_schema.table_constraints tc
124
- JOIN information_schema.key_column_usage kcu
125
- ON tc.constraint_name = kcu.constraint_name
126
- AND tc.table_schema = kcu.table_schema
127
- AND tc.table_name = kcu.table_name
128
- WHERE tc.table_schema = $1
129
- AND tc.table_name = $2
130
- AND tc.constraint_type = 'PRIMARY KEY'
131
- ORDER BY kcu.ordinal_position`,
132
- [schema, tableName],
133
- );
134
-
135
- const primaryKeyColumns = pkResult.rows
312
+ // Process primary key
313
+ const pkRows = [...(pksByTable.get(tableName) ?? [])];
314
+ const primaryKeyColumns = pkRows
136
315
  .sort((a, b) => a.ordinal_position - b.ordinal_position)
137
316
  .map((row) => row.column_name);
138
317
  const primaryKey: PrimaryKey | undefined =
139
318
  primaryKeyColumns.length > 0
140
319
  ? {
141
320
  columns: primaryKeyColumns,
142
- ...(pkResult.rows[0]?.constraint_name
143
- ? { name: pkResult.rows[0].constraint_name }
144
- : {}),
321
+ ...(pkRows[0]?.constraint_name ? { name: pkRows[0].constraint_name } : {}),
145
322
  }
146
323
  : undefined;
147
324
 
148
- // Query foreign keys
149
- const fkResult = await driver.query<{
150
- constraint_name: string;
151
- column_name: string;
152
- ordinal_position: number;
153
- referenced_table_schema: string;
154
- referenced_table_name: string;
155
- referenced_column_name: string;
156
- }>(
157
- `SELECT
158
- tc.constraint_name,
159
- kcu.column_name,
160
- kcu.ordinal_position,
161
- ccu.table_schema AS referenced_table_schema,
162
- ccu.table_name AS referenced_table_name,
163
- ccu.column_name AS referenced_column_name
164
- FROM information_schema.table_constraints tc
165
- JOIN information_schema.key_column_usage kcu
166
- ON tc.constraint_name = kcu.constraint_name
167
- AND tc.table_schema = kcu.table_schema
168
- AND tc.table_name = kcu.table_name
169
- JOIN information_schema.constraint_column_usage ccu
170
- ON ccu.constraint_name = tc.constraint_name
171
- AND ccu.table_schema = tc.table_schema
172
- WHERE tc.table_schema = $1
173
- AND tc.table_name = $2
174
- AND tc.constraint_type = 'FOREIGN KEY'
175
- ORDER BY tc.constraint_name, kcu.ordinal_position`,
176
- [schema, tableName],
177
- );
178
-
325
+ // Process foreign keys
179
326
  const foreignKeysMap = new Map<
180
327
  string,
181
328
  {
@@ -183,12 +330,13 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
183
330
  referencedTable: string;
184
331
  referencedColumns: string[];
185
332
  name: string;
333
+ deleteRule: string;
334
+ updateRule: string;
186
335
  }
187
336
  >();
188
- for (const fkRow of fkResult.rows) {
337
+ for (const fkRow of fksByTable.get(tableName) ?? []) {
189
338
  const existing = foreignKeysMap.get(fkRow.constraint_name);
190
339
  if (existing) {
191
- // Multi-column FK - add column
192
340
  existing.columns.push(fkRow.column_name);
193
341
  existing.referencedColumns.push(fkRow.referenced_column_name);
194
342
  } else {
@@ -197,6 +345,8 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
197
345
  referencedTable: fkRow.referenced_table_name,
198
346
  referencedColumns: [fkRow.referenced_column_name],
199
347
  name: fkRow.constraint_name,
348
+ deleteRule: fkRow.delete_rule,
349
+ updateRule: fkRow.update_rule,
200
350
  });
201
351
  }
202
352
  }
@@ -206,46 +356,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
206
356
  referencedTable: fk.referencedTable,
207
357
  referencedColumns: Object.freeze([...fk.referencedColumns]) as readonly string[],
208
358
  name: fk.name,
359
+ ...ifDefined('onDelete', mapReferentialAction(fk.deleteRule)),
360
+ ...ifDefined('onUpdate', mapReferentialAction(fk.updateRule)),
209
361
  }),
210
362
  );
211
363
 
212
- // Query unique constraints (excluding PK)
213
- const uniqueResult = await driver.query<{
214
- constraint_name: string;
215
- column_name: string;
216
- ordinal_position: number;
217
- }>(
218
- `SELECT
219
- tc.constraint_name,
220
- kcu.column_name,
221
- kcu.ordinal_position
222
- FROM information_schema.table_constraints tc
223
- JOIN information_schema.key_column_usage kcu
224
- ON tc.constraint_name = kcu.constraint_name
225
- AND tc.table_schema = kcu.table_schema
226
- AND tc.table_name = kcu.table_name
227
- WHERE tc.table_schema = $1
228
- AND tc.table_name = $2
229
- AND tc.constraint_type = 'UNIQUE'
230
- AND tc.constraint_name NOT IN (
231
- SELECT constraint_name
232
- FROM information_schema.table_constraints
233
- WHERE table_schema = $1
234
- AND table_name = $2
235
- AND constraint_type = 'PRIMARY KEY'
236
- )
237
- ORDER BY tc.constraint_name, kcu.ordinal_position`,
238
- [schema, tableName],
239
- );
240
-
241
- const uniquesMap = new Map<
242
- string,
243
- {
244
- columns: string[];
245
- name: string;
364
+ // Process unique constraints (excluding those that are also PKs)
365
+ const pkConstraints = pkConstraintsByTable.get(tableName) ?? new Set();
366
+ const uniquesMap = new Map<string, { columns: string[]; name: string }>();
367
+ for (const uniqueRow of uniquesByTable.get(tableName) ?? []) {
368
+ // Skip if this constraint is also a primary key
369
+ if (pkConstraints.has(uniqueRow.constraint_name)) {
370
+ continue;
246
371
  }
247
- >();
248
- for (const uniqueRow of uniqueResult.rows) {
249
372
  const existing = uniquesMap.get(uniqueRow.constraint_name);
250
373
  if (existing) {
251
374
  existing.columns.push(uniqueRow.column_name);
@@ -261,48 +384,9 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
261
384
  name: uq.name,
262
385
  }));
263
386
 
264
- // Query indexes (excluding PK and unique constraints)
265
- const indexResult = await driver.query<{
266
- indexname: string;
267
- indisunique: boolean;
268
- attname: string;
269
- attnum: number;
270
- }>(
271
- `SELECT
272
- i.indexname,
273
- ix.indisunique,
274
- a.attname,
275
- a.attnum
276
- FROM pg_indexes i
277
- JOIN pg_class ic ON ic.relname = i.indexname
278
- JOIN pg_namespace ins ON ins.oid = ic.relnamespace AND ins.nspname = $1
279
- JOIN pg_index ix ON ix.indexrelid = ic.oid
280
- JOIN pg_class t ON t.oid = ix.indrelid
281
- JOIN pg_namespace tn ON tn.oid = t.relnamespace AND tn.nspname = $1
282
- LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND a.attnum > 0
283
- WHERE i.schemaname = $1
284
- AND i.tablename = $2
285
- AND NOT EXISTS (
286
- SELECT 1
287
- FROM information_schema.table_constraints tc
288
- WHERE tc.table_schema = $1
289
- AND tc.table_name = $2
290
- AND tc.constraint_name = i.indexname
291
- )
292
- ORDER BY i.indexname, a.attnum`,
293
- [schema, tableName],
294
- );
295
-
296
- const indexesMap = new Map<
297
- string,
298
- {
299
- columns: string[];
300
- name: string;
301
- unique: boolean;
302
- }
303
- >();
304
- for (const idxRow of indexResult.rows) {
305
- // Skip rows where attname is null (system columns or invalid attnum)
387
+ // Process indexes
388
+ const indexesMap = new Map<string, { columns: string[]; name: string; unique: boolean }>();
389
+ for (const idxRow of indexesByTable.get(tableName) ?? []) {
306
390
  if (!idxRow.attname) {
307
391
  continue;
308
392
  }
@@ -326,30 +410,26 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
326
410
  tables[tableName] = {
327
411
  name: tableName,
328
412
  columns,
329
- ...(primaryKey ? { primaryKey } : {}),
413
+ ...ifDefined('primaryKey', primaryKey),
330
414
  foreignKeys,
331
415
  uniques,
332
416
  indexes,
333
417
  };
334
418
  }
335
419
 
336
- // Query extensions
337
- const extensionsResult = await driver.query<{
338
- extname: string;
339
- }>(
340
- `SELECT extname
341
- FROM pg_extension
342
- ORDER BY extname`,
343
- [],
344
- );
345
-
346
420
  const extensions = extensionsResult.rows.map((row) => row.extname);
347
421
 
348
- // Build annotations with Postgres-specific metadata
422
+ const storageTypes =
423
+ (await pgEnumControlHooks.introspectTypes?.({ driver, schemaName: schema })) ?? {};
424
+
349
425
  const annotations = {
350
426
  pg: {
351
427
  schema,
352
428
  version: await this.getPostgresVersion(driver),
429
+ ...ifDefined(
430
+ 'storageTypes',
431
+ Object.keys(storageTypes).length > 0 ? storageTypes : undefined,
432
+ ),
353
433
  },
354
434
  };
355
435
 
@@ -373,3 +453,145 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
373
453
  return match?.[1] ?? 'unknown';
374
454
  }
375
455
  }
456
+
457
+ /**
458
+ * Pre-computed lookup map for simple prefix-based type normalization.
459
+ * Maps short Postgres type names to their canonical SQL names.
460
+ * Using a Map for O(1) lookup instead of multiple startsWith checks.
461
+ */
462
+ const TYPE_PREFIX_MAP: ReadonlyMap<string, string> = new Map([
463
+ ['varchar', 'character varying'],
464
+ ['bpchar', 'character'],
465
+ ['varbit', 'bit varying'],
466
+ ]);
467
+
468
+ /**
469
+ * Normalizes a Postgres schema native type to its canonical form for comparison.
470
+ *
471
+ * Uses a pre-computed lookup map for simple prefix replacements (O(1))
472
+ * and handles complex temporal type normalization separately.
473
+ */
474
+ export function normalizeSchemaNativeType(nativeType: string): string {
475
+ const trimmed = nativeType.trim();
476
+
477
+ // Fast path: check simple prefix replacements using the lookup map
478
+ for (const [prefix, replacement] of TYPE_PREFIX_MAP) {
479
+ if (trimmed.startsWith(prefix)) {
480
+ return replacement + trimmed.slice(prefix.length);
481
+ }
482
+ }
483
+
484
+ // Temporal types with time zone handling
485
+ // Check for 'with time zone' suffix first (more specific)
486
+ if (trimmed.includes(' with time zone')) {
487
+ if (trimmed.startsWith('timestamp')) {
488
+ return `timestamptz${trimmed.slice(9).replace(' with time zone', '')}`;
489
+ }
490
+ if (trimmed.startsWith('time')) {
491
+ return `timetz${trimmed.slice(4).replace(' with time zone', '')}`;
492
+ }
493
+ }
494
+
495
+ // Handle 'without time zone' suffix - just strip it
496
+ if (trimmed.includes(' without time zone')) {
497
+ return trimmed.replace(' without time zone', '');
498
+ }
499
+
500
+ return trimmed;
501
+ }
502
+
503
+ function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
504
+ if (formattedType === 'integer') {
505
+ return 'int4';
506
+ }
507
+ if (formattedType === 'smallint') {
508
+ return 'int2';
509
+ }
510
+ if (formattedType === 'bigint') {
511
+ return 'int8';
512
+ }
513
+ if (formattedType === 'real') {
514
+ return 'float4';
515
+ }
516
+ if (formattedType === 'double precision') {
517
+ return 'float8';
518
+ }
519
+ if (formattedType === 'boolean') {
520
+ return 'bool';
521
+ }
522
+ if (formattedType.startsWith('varchar')) {
523
+ return formattedType.replace('varchar', 'character varying');
524
+ }
525
+ if (formattedType.startsWith('bpchar')) {
526
+ return formattedType.replace('bpchar', 'character');
527
+ }
528
+ if (formattedType.startsWith('varbit')) {
529
+ return formattedType.replace('varbit', 'bit varying');
530
+ }
531
+ if (dataType === 'timestamp with time zone' || udtName === 'timestamptz') {
532
+ return formattedType.replace('timestamp', 'timestamptz').replace(' with time zone', '').trim();
533
+ }
534
+ if (dataType === 'timestamp without time zone' || udtName === 'timestamp') {
535
+ return formattedType.replace(' without time zone', '').trim();
536
+ }
537
+ if (dataType === 'time with time zone' || udtName === 'timetz') {
538
+ return formattedType.replace('time', 'timetz').replace(' with time zone', '').trim();
539
+ }
540
+ if (dataType === 'time without time zone' || udtName === 'time') {
541
+ return formattedType.replace(' without time zone', '').trim();
542
+ }
543
+ // Only dataType === 'USER-DEFINED' should ever be quoted, but this should be safe without
544
+ // checking that explicitly either way
545
+ if (formattedType.startsWith('"') && formattedType.endsWith('"')) {
546
+ return formattedType.slice(1, -1);
547
+ }
548
+ return formattedType;
549
+ }
550
+
551
+ /**
552
+ * The five standard PostgreSQL referential action rules as returned by
553
+ * `information_schema.referential_constraints.delete_rule` / `update_rule`.
554
+ */
555
+ type PgReferentialActionRule = 'NO ACTION' | 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'SET DEFAULT';
556
+
557
+ const PG_REFERENTIAL_ACTION_MAP: Record<PgReferentialActionRule, SqlReferentialAction> = {
558
+ 'NO ACTION': 'noAction',
559
+ RESTRICT: 'restrict',
560
+ CASCADE: 'cascade',
561
+ 'SET NULL': 'setNull',
562
+ 'SET DEFAULT': 'setDefault',
563
+ };
564
+
565
+ /**
566
+ * Maps a Postgres referential action rule to the canonical SqlReferentialAction.
567
+ * Returns undefined for 'NO ACTION' (the database default) to keep the IR sparse.
568
+ * Throws for unrecognized rules to prevent silent data loss.
569
+ */
570
+ function mapReferentialAction(rule: string): SqlReferentialAction | undefined {
571
+ const mapped = PG_REFERENTIAL_ACTION_MAP[rule as PgReferentialActionRule];
572
+ if (mapped === undefined) {
573
+ throw new Error(
574
+ `Unknown PostgreSQL referential action rule: "${rule}". Expected one of: NO ACTION, RESTRICT, CASCADE, SET NULL, SET DEFAULT.`,
575
+ );
576
+ }
577
+ if (mapped === 'noAction') return undefined;
578
+ return mapped;
579
+ }
580
+
581
+ /**
582
+ * Groups an array of objects by a specified key.
583
+ * Returns a Map for O(1) lookup by group key.
584
+ */
585
+ function groupBy<T, K extends keyof T>(items: readonly T[], key: K): Map<T[K], T[]> {
586
+ const map = new Map<T[K], T[]>();
587
+ for (const item of items) {
588
+ const groupKey = item[key];
589
+ let group = map.get(groupKey);
590
+ if (!group) {
591
+ group = [];
592
+ map.set(groupKey, group);
593
+ }
594
+ group.push(item);
595
+ }
596
+ return map;
597
+ }