@prisma-next/adapter-postgres 0.3.0-dev.11 → 0.3.0-dev.114

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